isolate.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { Context } from '@deepseek-ai/cordis'
  2. import type { Dict } from '@deepseek-ai/cosmokit'
  3. import { Entry } from './entry.ts'
  4. declare module './entry.ts' {
  5. interface EntryOptions {
  6. intercept?: Dict | null
  7. isolate?: Dict<true | string> | null
  8. }
  9. interface Entry {
  10. realm: LocalRealm
  11. }
  12. }
  13. function swap<T extends {}>(target: T, source?: T | null) {
  14. for (const key of Reflect.ownKeys(target)) {
  15. Reflect.deleteProperty(target, key)
  16. }
  17. for (const key of Reflect.ownKeys(source || {})) {
  18. Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source!, key)!)
  19. }
  20. }
  21. /** Symbol realm used to isolate service implementations by entry or label. */
  22. export abstract class Realm {
  23. protected store: Dict<symbol> = Object.create(null)
  24. abstract get suffix(): string
  25. access(key: string, create = false) {
  26. if (create) {
  27. return this.store[key] ??= Symbol(`${key}${this.suffix}`)
  28. } else {
  29. return this.store[key] ?? Symbol(`${key}${this.suffix}`)
  30. }
  31. }
  32. delete(key: string) {
  33. delete this.store[key]
  34. }
  35. get size() {
  36. return Object.keys(this.store).length
  37. }
  38. }
  39. /** Entry-local isolation realm. */
  40. export class LocalRealm extends Realm {
  41. constructor(private entry: Entry) {
  42. super()
  43. }
  44. get suffix() {
  45. return '#' + this.entry.options.id
  46. }
  47. }
  48. /** Named isolation realm shared by entries that use the same label. */
  49. export class GlobalRealm extends Realm {
  50. constructor(public label: string) {
  51. super()
  52. }
  53. get suffix() {
  54. return '@' + this.label
  55. }
  56. }
  57. /** Install loader hooks that apply `intercept` and `isolate` entry options. */
  58. export default function isolate(ctx: Context) {
  59. const realms: Dict<GlobalRealm> = Object.create(null)
  60. const delims: Dict<symbol> = Object.create(null)
  61. function access(entry: Entry, name: string, create: true): symbol
  62. function access(entry: Entry, name: string, create?: boolean): symbol | undefined
  63. function access(entry: Entry, name: string, create = false) {
  64. let realm: Realm | undefined
  65. const label = entry.options.isolate?.[name]
  66. if (!label) return
  67. if (label === true) {
  68. realm = entry.realm ??= new LocalRealm(entry)
  69. } else if (create) {
  70. realm = realms[label] ??= new GlobalRealm(label)
  71. } else {
  72. realm = realms[label]
  73. }
  74. return realm?.access(name, create)
  75. }
  76. ctx.on('loader/entry-init', (entry) => {
  77. entry.ctx[Context.intercept] = Object.create(entry.ctx[Context.intercept])
  78. entry.ctx[Context.isolate] = Object.create(entry.ctx[Context.isolate])
  79. })
  80. ctx.on('loader/patch-context', (entry, next) => {
  81. // step 1: generate new isolate map
  82. const newMap: Dict<symbol> = Object.create(entry.parent.ctx[Context.isolate])
  83. for (const name of Object.keys(entry.options.isolate ?? {})) {
  84. newMap[name] = access(entry, name, true)
  85. }
  86. // step 2: generate service diff
  87. const diff: Dict<[symbol, symbol, symbol, symbol]> = Object.create(null)
  88. const oldMap = entry.ctx[Context.isolate]
  89. for (const name in { ...newMap, ...delims }) {
  90. if (newMap[name] === oldMap[name]) continue
  91. const delim = delims[name] ??= Symbol(`delim:${name}`)
  92. entry.ctx[delim] = Symbol(`${name}#${entry.id}`)
  93. for (const symbol of [oldMap[name], newMap[name]]) {
  94. const impl = symbol && entry.ctx.reflect.store[symbol]
  95. if (!impl) continue
  96. if (!impl.fiber) {
  97. entry.ctx.logger.warn(new Error(`expected service ${name} to be implemented`))
  98. continue
  99. }
  100. diff[name] = [oldMap[name], newMap[name], entry.ctx[delim], impl.fiber.ctx[delim]]
  101. if (entry.ctx[delim] !== impl.fiber.ctx[delim]) break
  102. }
  103. }
  104. // step 3: set prototype for transferred context
  105. Object.setPrototypeOf(entry.ctx[Context.isolate], entry.parent.ctx[Context.isolate])
  106. Object.setPrototypeOf(entry.ctx[Context.intercept], entry.parent.ctx[Context.intercept])
  107. swap(entry.ctx[Context.isolate], newMap)
  108. swap(entry.ctx[Context.intercept], entry.options.intercept)
  109. // step 4: reload fiber
  110. next()
  111. // step 5: replace service impl
  112. for (const [symbol1, symbol2, flag1, flag2] of Object.values(diff)) {
  113. if (flag1 === flag2 && entry.ctx.reflect.store[symbol1] && !entry.ctx.reflect.store[symbol2]) {
  114. entry.ctx.reflect.store[symbol2] = entry.ctx.reflect.store[symbol1]
  115. delete entry.ctx.reflect.store[symbol1]
  116. }
  117. }
  118. // step 6: reflect notify
  119. ctx.reflect.notify(Object.keys(diff), (ctx, name) => {
  120. const [symbol1, symbol2, flag1, flag2] = diff[name]
  121. const symbol3 = ctx[Context.isolate][name]
  122. const flag3 = ctx[delims[name]]
  123. return (symbol1 === symbol3 || symbol2 === symbol3) && (flag1 === flag3) !== (flag1 === flag2)
  124. })
  125. // step 7: clean up delimiters
  126. for (const name in delims) {
  127. if (!Reflect.ownKeys(newMap).includes(name)) {
  128. delete entry.ctx[delims[name]]
  129. }
  130. }
  131. })
  132. ctx.on('loader/partial-dispose', (entry, legacy, active) => {
  133. for (const [name, label] of Object.entries(legacy.isolate ?? {})) {
  134. if (label === true) continue
  135. if (active && entry.options.isolate?.[name] === label) continue
  136. const realm = realms[label]
  137. if (!realm) continue
  138. // realm garbage collection
  139. for (const entry of ctx.loader.entries()) {
  140. // has reference to this realm
  141. if (entry.options.isolate?.[name] === realm.label) return
  142. }
  143. realm.delete(name)
  144. if (!realm.size) {
  145. delete realms[realm.label]
  146. }
  147. }
  148. })
  149. }