@netless/app-presentation 0.1.7 → 0.1.8-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/app-presentation",
3
- "version": "0.1.7",
3
+ "version": "0.1.8-beta.0",
4
4
  "description": "Netless App for presentation slides (images)",
5
5
  "author": "netless",
6
6
  "license": "MIT",
@@ -1,13 +1,16 @@
1
- import type { AnimationMode, AppContext, NetlessApp, ReadonlyTeleBox, Room, SceneDefinition, Storage, View, WindowManager } from "@netless/window-manager"
1
+ import type { AnimationMode, AppContext, AppPayload, NetlessApp, PublicEvent, ReadonlyTeleBox, Room, SceneDefinition, View, WindowManager } from "@netless/window-manager"
2
2
 
3
3
  import { disposableStore } from '@wopjs/disposable'
4
4
  import { listen } from '@wopjs/dom'
5
5
 
6
6
  import styles from './style.scss?inline'
7
7
  import { Presentation, type PresentationConfig, type PresentationPage } from "./presentation";
8
+ import { readable, type Readable } from "./store";
8
9
 
9
10
  export type Logger = (...data: any[]) => void
10
11
 
12
+ const emptySceneName = '$$empty$$'
13
+
11
14
  interface Viewport {
12
15
  readonly x: number;
13
16
  readonly y: number;
@@ -71,6 +74,18 @@ const createLogger = (room: Room | undefined): Logger => {
71
74
  return (...args) => console.log(...args)
72
75
  }
73
76
  }
77
+ ``
78
+ const scenesEqual = (scenes1?: SceneDefinition[], scenes2?: SceneDefinition[]): boolean => {
79
+ if (!scenes1 || !scenes2) {return false}
80
+ if (scenes1.length !== scenes2.length) return false;
81
+ return scenes1.every((scene, index) => {
82
+ const scene2 = scenes2[index];
83
+ return scene.name === scene2.name &&
84
+ scene.ppt?.width === scene2.ppt?.width &&
85
+ scene.ppt?.height === scene2.ppt?.height &&
86
+ scene.ppt?.src === scene2.ppt?.src;
87
+ });
88
+ };
74
89
 
75
90
  export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions, PresentationController> = {
76
91
  kind: "Presentation",
@@ -99,45 +114,107 @@ export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions,
99
114
  const log = options.log || createLogger(context.getRoom())
100
115
  log(`[Presentation] new ${context.appId}`)
101
116
 
117
+ const dispose = disposableStore()
118
+ dispose.add(() => log(`[Presentation] dispose ${context.appId}`))
119
+
120
+ const view$$ = context.createStorage('view', { uid: "", originX: 0, originY: 0, width: 0, height: 0 })
121
+
122
+
123
+ const _addScenePathListener = (
124
+ name: keyof PublicEvent,
125
+ listener: any
126
+ ) => {
127
+ const windowManger = (context as any).manager.windowManger as WindowManager;
128
+ windowManger.emitter.on(name, listener)
129
+ return () => windowManger?.emitter.off(name, listener)
130
+ }
131
+
132
+ const getPageIndex = (view: View) => {
133
+ const focusScenePath = view.focusScenePath;
134
+ const name = focusScenePath?.split('/').pop();
135
+ let _pageIndex = pages.findIndex((page, index) => {
136
+ const n = page.name ?? String(index + 1);
137
+ return n === name;
138
+ });
139
+ if (_pageIndex === -1) {
140
+ _pageIndex = 0;
141
+ }
142
+ return _pageIndex;
143
+ }
144
+
145
+ let pageIndex = getPageIndex(view);
146
+ const pageIndex$ = readable<number>(pageIndex, set => {
147
+ set(pageIndex);
148
+ return _addScenePathListener("onAppScenePathChange", (payload: AppPayload)=>{
149
+ const { appId } = payload;
150
+ if (appId === context.appId) {
151
+ const _pageIndex = getPageIndex(payload.view);
152
+ set(_pageIndex)
153
+ }
154
+ });
155
+ });
156
+
157
+ dispose.add(() => {
158
+ pageIndex$.dispose();
159
+ })
160
+
102
161
  // Prepare scenes.
103
162
  // Caution: some user may insert a 500-page PDF.
104
163
  if (context.isAddApp) {
105
164
  if (pages.length > 100)
106
165
  console.warn(`[Presentation]: too many pages (${pages.length}), may cause performance issues`)
107
166
 
108
- const room = context.getRoom()
167
+ let redirectResolve: ((bol:boolean) => void) | undefined = undefined;
168
+ const room = context.getRoom();
109
169
  if (room && room.isWritable) {
110
170
  const scenes = room.entireScenes()[scenePath];
111
- for (let i = 0; i < pages.length; i++) {
112
- const p = pages[i];
113
- const name = p.name ?? String(i + 1);
114
- const scene = scenes.find(scene => scene.name == name && scene.ppt)
115
- if (scene) {
116
- if (scene.ppt && scene.ppt?.src === p.src) {
117
- continue;
118
- }
119
- room.removeScenes(`${scenePath}/${name}`)
120
- }
121
- room.putScenes(scenePath, pages.map((p, index) => ({
171
+ if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
172
+ throw new Error(`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`);
173
+ }
174
+ new Promise((resolve) => {
175
+ const {name, ppt} = scenes[pageIndex$.value];
176
+ redirectResolve = resolve;
177
+ const _scenes = pages.map((p, index) => ({
122
178
  name: p.name ?? String(index + 1),
123
179
  ppt: { width: p.width, height: p.height, src: p.src }
124
- })))
125
- }
180
+ }))
181
+
182
+ if (!scenesEqual(scenes, _scenes)) {
183
+ room.removeScenes(scenePath)
184
+ room.putScenes(scenePath, _scenes)
185
+ }
186
+ if(name === _scenes[pageIndex$.value].name && !ppt){
187
+ context.addPage({ scene: { name: emptySceneName } }).then(() => {
188
+ log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
189
+ context.setScenePath(`${scenePath}/${emptySceneName}`).then(()=>{
190
+ redirectResolve && redirectResolve(true);
191
+ })
192
+ });
193
+ } else {
194
+ redirectResolve && redirectResolve(false)
195
+ }
196
+ }).then(async(bol)=>{
197
+ await syncPage(pageIndex$.value, (room as any).logger);
198
+ if (bol) {
199
+ log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
200
+ room.removeScenes(`${scenePath}/${emptySceneName}`);
201
+ }
202
+ });
126
203
  }
127
204
  }
128
205
 
129
- const dispose = disposableStore()
130
- dispose.add(() => log(`[Presentation] dispose ${context.appId}`))
206
+ // let lastIndex = -1
131
207
 
132
- const page$$ = context.createStorage('page', { index: 0 })
133
- const view$$ = context.createStorage('view', { uid: "", originX: 0, originY: 0, width: 0, height: 0 })
208
+ const me = context.getRoom()?.uid || context.getDisplayer().observerId + ''
134
209
 
135
- let lastIndex = -1
136
- const syncPage = async (index: number) => {
137
- if (lastIndex !== index) {
138
- lastIndex = index
139
- context.dispatchAppEvent('pageStateChange', { index, length: pages.length })
140
- }
210
+ let throttleSyncView = 0
211
+
212
+ const syncPage = async (index: number, logger?: any) => {
213
+ // if (lastIndex !== index) {
214
+ // lastIndex = index
215
+ // console.log('dispatchAppEvent===>pageStateChange', index);
216
+ // context.dispatchAppEvent('pageStateChange', { index, length: pages.length })
217
+ // }
141
218
 
142
219
  if (!context.getIsWritable()) return
143
220
 
@@ -153,6 +230,10 @@ export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions,
153
230
  await context.addPage({ scene: { name, ppt: { width: p.width, height: p.height, src: p.src } } })
154
231
  }
155
232
 
233
+ if (logger) {
234
+ logger.info(`[Presentation] syncPage ${scenePath}/${name}`);
235
+ }
236
+
156
237
  // Switch to that page.
157
238
  await context.setScenePath(`${scenePath}/${name}`)
158
239
  }
@@ -178,16 +259,16 @@ export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions,
178
259
  const name = p.name ?? String(index + 1);
179
260
 
180
261
  if (!scenes.some(scene => scene.name === name)) {
181
- context.addPage({ scene: { name } })
262
+ context.addPage({ scene: { name, ppt: { width: p.width, height: p.height, src: p.src } } })
182
263
  }
183
264
 
184
- page$$.setState({ index })
265
+ syncPage(index);
185
266
  return true
186
267
  }
187
268
 
188
- const prevPage = () => jumpPage(page$$.state.index - 1)
189
- const nextPage = () => jumpPage(page$$.state.index + 1)
190
- const pageState = () => ({ index: page$$.state.index, length: pages.length })
269
+ const prevPage = () => jumpPage(pageIndex$.value - 1)
270
+ const nextPage = () => jumpPage(pageIndex$.value + 1)
271
+ const pageState = () => ({ index: pageIndex$.value, length: pages.length })
191
272
 
192
273
  const scaleDocsToFit = () => {
193
274
  const { width, height } = app.page() || {}
@@ -207,10 +288,6 @@ export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions,
207
288
  syncViewFromRemote(true)
208
289
  }
209
290
  }
210
-
211
- const me = context.getRoom()?.uid || context.getDisplayer().observerId + ''
212
-
213
- let throttleSyncView = 0
214
291
  const syncView = () => {
215
292
  if (throttleSyncView > 0) return
216
293
  const { width, height } = app.page() || {}
@@ -243,12 +320,10 @@ export const NetlessAppPresentation: NetlessApp<{}, {}, PresentationAppOptions,
243
320
  }
244
321
  }
245
322
 
246
- syncPage(page$$.state.index)
247
- dispose.add(page$$.addStateChangedListener(() => syncPage(page$$.state.index)))
248
323
  dispose.add(view$$.addStateChangedListener(() => syncViewFromRemote(false, true)))
249
324
 
250
325
  const box = context.getBox()
251
- const app = dispose.add(createPresentation(box, pages, jumpPage, page$$, options.thumbnail))
326
+ const app = dispose.add(createPresentation(box, pages, jumpPage, pageIndex$, options.thumbnail))
252
327
  app.contentDOM.dataset.appPresentationVersion = __VERSION__
253
328
  app.scaleDocsToFit = scaleDocsToFit
254
329
  app.log = log
@@ -444,6 +519,7 @@ class AppPresentation extends Presentation {
444
519
 
445
520
  override updateImage() {
446
521
  // Do nothing, the image was set in the whiteboard scene.
522
+ super.updateImage();
447
523
  }
448
524
 
449
525
  override onNewPageIndex(index: number, origin: "navigation" | "keydown" | "input" | "preview") {
@@ -465,7 +541,7 @@ function createPresentation(
465
541
  box: ReadonlyTeleBox,
466
542
  pages: PresentationPage[],
467
543
  jumpPage: (index: number) => void,
468
- page$$: Storage<{ index: number }>,
544
+ pageIndex$: Readable<number>,
469
545
  thumbnail?: (src: string) => string,
470
546
  ): AppPresentation {
471
547
  box.mountStyles(styles)
@@ -475,10 +551,7 @@ function createPresentation(
475
551
  box.mountContent(app.contentDOM)
476
552
  box.mountFooter(app.footerDOM)
477
553
 
478
- app.setPageIndex(page$$.state.index)
479
- app.dispose.add(page$$.addStateChangedListener(() => {
480
- app.setPageIndex(page$$.state.index)
481
- }))
554
+ app.dispose.add(pageIndex$.subscribe(pageIndex => { app.setPageIndex(pageIndex) }))
482
555
 
483
556
  return app
484
557
  }
package/src/preload.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  import type { IDisposable } from "@wopjs/disposable";
2
2
  import type { PresentationPage } from "./presentation";
3
3
 
4
+ export type Subscriber<T> = (value: T) => void;
5
+ export type Unsubscriber = () => void;
6
+ export type Updater<T> = (value: T) => T;
7
+
8
+ export interface PageIndex<T> {
9
+ readonly value: T;
10
+ subscribe(this: void, run: Subscriber<T>): Unsubscriber;
11
+ reaction(this: void, run: Subscriber<T>): Unsubscriber;
12
+ dispose(value?: T): void;
13
+ }
14
+
4
15
  export enum ELoadState {
5
16
  unloaded,
6
- loading,
7
17
  loaded,
8
18
  error
9
19
  }
@@ -15,10 +25,14 @@ export interface IPreloadMapValue {
15
25
 
16
26
  export class Preload implements IDisposable {
17
27
  static maxLinks: number = 5;
18
- readonly loadingLinks: Map<number, HTMLLinkElement> = new Map();
28
+ // 正在加载的链接
29
+ readonly loadingLinks: Map<number, {
30
+ link: HTMLLinkElement,
31
+ isForce: boolean
32
+ }> = new Map();
33
+ // 需要预加载的链接集合
19
34
  readonly preloadMap = new Map<number, IPreloadMapValue>();
20
- readonly waitingLoadSet: Set<number> = new Set();
21
- readonly waitinglinks: number[] = [];
35
+ // 当前触摸的索引
22
36
  touchIndex: number = 0;
23
37
  preloadSize: number = 0;
24
38
  constructor(readonly pages: PresentationPage[]) {
@@ -26,118 +40,87 @@ export class Preload implements IDisposable {
26
40
  src: e.src,
27
41
  state: ELoadState.unloaded
28
42
  }]));
29
- this.waitingLoadSet = new Set(pages.map((e,index) => index));
30
43
  this.preloadSize = this.preloadMap.size;
31
- this.touch(0);
32
- }
33
- async touch(index: number): Promise<ELoadState> {
34
- this.touchIndex = index;
35
- const value = this.preloadMap.get(index);
36
- let state = ELoadState.unloaded;
37
- if (value) {
38
- state = value.state;
39
- if (value.state === ELoadState.unloaded) {
40
- if (this.loadingLinks.size) {
41
- for (const [i,link] of this.loadingLinks) {
42
- this.destroyLink(i, link);
43
- this.waitingLoadSet.add(i);
44
- const v = this.preloadMap.get(i);
45
- if (v) {
46
- v.state = ELoadState.unloaded;
47
- this.preloadMap.set(i, v);
48
- }
49
- }
50
- for (const i of this.waitinglinks) {
51
- this.waitingLoadSet.add(i);
52
- }
53
- this.waitinglinks.length = 0;
54
- }
55
- if (this.waitinglinks.includes(index)) {
56
- this.waitinglinks.splice(this.waitinglinks.indexOf(index), 1);
57
- }
58
- state = await new Promise<ELoadState>(resolve => {
59
- this.waitingLoadSet.delete(index);
60
- this.createLink(index, resolve);
61
- })
62
- }
63
- }
64
- if (this.waitingLoadSet.size) {
65
- this.requestAsyncCallBack(()=>{
66
- this.autoTouch();
67
- }, 1000)
68
- }
69
- return state;
44
+ this.touch(0, true);
70
45
  }
71
- private autoTouch(){
72
- this.handler();
73
- if (this.waitingLoadSet.size === 0) {
74
- return;
46
+ touch(index: number, force: boolean = false) {
47
+ if (index >= this.preloadSize) {
48
+ this.touchIndex = 0;
49
+ } else {
50
+ this.touchIndex = index;
75
51
  }
76
- let nextTouchDelay = 1000;
77
- this.touchIndex = (this.touchIndex + 1) % this.preloadSize;
78
52
  const value = this.preloadMap.get(this.touchIndex);
79
- if (value) {
80
- switch (value.state) {
81
- case ELoadState.unloaded:
82
- case ELoadState.error:
83
- if (!this.waitinglinks.includes(this.touchIndex)) {
84
- this.waitinglinks.push(this.touchIndex);
85
- this.waitingLoadSet.delete(this.touchIndex);
86
- }
87
- nextTouchDelay = 0;
88
- break;
53
+ if (value && value.state === ELoadState.unloaded) {
54
+ this.createLink(this.touchIndex, force);
55
+ }
56
+ if (this.loadingLinks.size < Preload.maxLinks) {
57
+ const willLoad = [...this.preloadMap.entries()].find(([i, e]) => i > this.touchIndex && e.state !== ELoadState.loaded);
58
+ if (willLoad) {
59
+ this.requestAsyncCallBack(()=>{this.touch(willLoad[0], false)}, 100);
89
60
  }
90
61
  }
91
- this.requestAsyncCallBack(()=>{
92
- this.autoTouch();
93
- }, nextTouchDelay)
94
62
  }
95
63
 
96
- private createLink(index: number, resolve?:(state:ELoadState)=>void) {
64
+ private createLink(index: number, force: boolean = false):ELoadState {
97
65
  const value = this.preloadMap.get(index);
66
+ if (force) {
67
+ this.destroySomeLink(index);
68
+ }
98
69
  if (value) {
99
- const link = document.createElement('link');
100
- link.rel = 'preload';
101
- link.as = 'image';
102
- link.href = value.src;
103
- link.dataset.order = index + '';
104
- document.head.appendChild(link);
105
- link.onload = () => {
106
- value.state = ELoadState.loaded;
107
- this.preloadMap.set(index, value);
108
- document.head.contains(link) && document.head.removeChild(link);
109
- this.loadingLinks.delete(index);
110
- resolve && resolve(ELoadState.loaded);
111
- this.autoTouch();
70
+ if (value.state === ELoadState.loaded) {
71
+ return ELoadState.loaded;
72
+ }
73
+ const curlink = this.loadingLinks.get(index);
74
+ if (curlink) {
75
+ if (curlink.isForce !== force) {
76
+ curlink.isForce = force;
77
+ this.loadingLinks.set(index, curlink);
78
+ }
79
+ return ELoadState.unloaded;
80
+ }
81
+ if (this.loadingLinks.size > Preload.maxLinks) {
82
+ return ELoadState.unloaded;
83
+ }
84
+ const linkDom = document.createElement('link');
85
+ linkDom.rel = 'preload';
86
+ linkDom.as = 'image';
87
+ linkDom.href = value.src;
88
+ linkDom.dataset.order = index + '';
89
+ document.head.appendChild(linkDom);
90
+ linkDom.onload = () => {
91
+ const value = this.preloadMap.get(index);
92
+ if (value) {
93
+ value.state = ELoadState.loaded;
94
+ this.preloadMap.set(index, value);
95
+ document.head.contains(linkDom) && document.head.removeChild(linkDom);
96
+ this.loadingLinks.delete(index);
97
+ this.touch(this.touchIndex + 1, false);
98
+ }
112
99
  }
113
- link.onerror = () => {
114
- value.state = ELoadState.error;
115
- this.preloadMap.set(index, value);
116
- if (!this.waitinglinks.includes(index)) {
117
- this.waitinglinks.push(index);
118
- this.waitingLoadSet.delete(this.touchIndex);
100
+ linkDom.onerror = () => {
101
+ const value = this.preloadMap.get(index);
102
+ if (value?.src) {
103
+ linkDom.href = value.src;
119
104
  }
120
- document.head.contains(link) && document.head.removeChild(link);
121
- this.loadingLinks.delete(index);
122
- resolve && resolve(ELoadState.error);
123
- this.autoTouch();
124
105
  }
125
- this.loadingLinks.set(index, link);
126
- value.state = ELoadState.loading;
106
+ this.loadingLinks.set(index, {
107
+ link: linkDom,
108
+ isForce: force
109
+ });
110
+ value.state = ELoadState.unloaded;
127
111
  this.preloadMap.set(index, value);
112
+ return ELoadState.unloaded;
128
113
  }
114
+ return ELoadState.error;
129
115
  }
130
- private handler() {
131
- if(!this.waitinglinks.length) {
132
- return;
133
- }
134
- let i = this.loadingLinks.size;
135
- while (i < Preload.maxLinks) {
136
- const index = this.waitinglinks.shift();
137
- if (index !== undefined) {
138
- this.createLink(index);
116
+
117
+ private destroySomeLink(excludeIndex?: number) {
118
+ for (const [index, { link }] of this.loadingLinks) {
119
+ if (excludeIndex !== undefined && index === excludeIndex) {
120
+ continue;
139
121
  }
140
- i++;
122
+ document.head.contains(link) && document.head.removeChild(link);
123
+ this.loadingLinks.delete(index);
141
124
  }
142
125
  }
143
126
  private async requestAsyncCallBack (callBack:()=>void, timeout:number):Promise<void> {
@@ -154,16 +137,8 @@ export class Preload implements IDisposable {
154
137
  });
155
138
  callBack();
156
139
  }
157
- private destroyLink(index: number, link: HTMLLinkElement) {
158
- document.head.contains(link) && document.head.removeChild(link)
159
- this.loadingLinks.delete(index)
160
- }
161
140
  dispose() {
162
- for (const [index,link] of this.loadingLinks) {
163
- this.destroyLink(index, link);
164
- }
141
+ this.destroySomeLink();
165
142
  this.preloadMap.clear();
166
- this.waitingLoadSet.clear();
167
- this.waitinglinks.length = 0;
168
143
  }
169
144
  }
@@ -11,7 +11,7 @@ export interface PresentationPage {
11
11
  width: number;
12
12
  height: number;
13
13
  thumbnail?: string | undefined;
14
- name?: string | undefined;
14
+ name?: string;
15
15
  }
16
16
 
17
17
  export interface PresentationConfig {
@@ -298,16 +298,15 @@ export class Presentation implements IDisposable<void> {
298
298
  }
299
299
  this.timer = setTimeout(() => {
300
300
  this.timer = null;
301
- this.preload.touch(this.pageIndex).then(() => {
302
- const page = this.page()
303
- if (!page) {
304
- this.image.src = ''
305
- return
306
- }
307
- this.image.width = page.width
308
- this.image.height = page.height
309
- this.image.src = page.src
310
- })
301
+ this.preload.touch(this.pageIndex, true);
302
+ // const page = this.page()
303
+ // if (!page) {
304
+ // this.image.src = ''
305
+ // return
306
+ // }
307
+ // this.image.width = page.width
308
+ // this.image.height = page.height
309
+ // this.image.src = page.src
311
310
  }, 200)
312
311
  }
313
312
 
package/src/store.ts ADDED
@@ -0,0 +1,103 @@
1
+ // This is a simple mimic of svelte/store.
2
+ export type Subscriber<T> = (value: T) => void;
3
+ export type Unsubscriber = () => void;
4
+ export type Updater<T> = (value: T) => T;
5
+ export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void;
6
+
7
+ export interface Readable<T> {
8
+ readonly value: T;
9
+ subscribe(this: void, run: Subscriber<T>): Unsubscriber;
10
+ reaction(this: void, run: Subscriber<T>): Unsubscriber;
11
+ dispose(value?: T): void;
12
+ }
13
+
14
+ export interface Writable<T> extends Readable<T> {
15
+ set(this: void, value: T): void;
16
+ update(this: void, updater: Updater<T>): void;
17
+ }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
20
+ function noop() {}
21
+
22
+ function safe_not_equal(a: unknown, b: unknown) {
23
+ return a != a ? b == b : a !== b || (a && typeof a === "object") || typeof a === "function";
24
+ }
25
+
26
+ export function readable<T>(value: T, start: StartStopNotifier<T> = noop): Readable<T> {
27
+ let stop: Unsubscriber | undefined;
28
+ const subscribers = new Set<Subscriber<T>>();
29
+ function set(new_value: T) {
30
+ if (safe_not_equal(value, new_value)) {
31
+ value = new_value;
32
+ if (stop) {
33
+ for (const run of subscribers) {
34
+ run(value);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ function subscribe(run: Subscriber<T>) {
40
+ subscribers.add(run);
41
+ if (subscribers.size === 1) {
42
+ stop = start(set) || noop;
43
+ }
44
+ run(value);
45
+ return () => {
46
+ subscribers.delete(run);
47
+ if (subscribers.size === 0) {
48
+ stop && stop();
49
+ stop = undefined;
50
+ }
51
+ };
52
+ }
53
+ function reaction(run: Subscriber<T>) {
54
+ subscribers.add(run);
55
+ if (subscribers.size === 1) {
56
+ stop = start(set) || noop;
57
+ }
58
+ return () => {
59
+ subscribers.delete(run);
60
+ if (subscribers.size === 0) {
61
+ stop && stop();
62
+ stop = undefined;
63
+ }
64
+ };
65
+ }
66
+ function dispose(new_value?: T) {
67
+ if (new_value !== undefined) {
68
+ set(new_value);
69
+ }
70
+ subscribers.clear();
71
+ stop && stop();
72
+ stop = undefined;
73
+ }
74
+ return {
75
+ get value() {
76
+ if (subscribers.size === 0) {
77
+ stop = start(set) || noop;
78
+ stop();
79
+ stop = undefined;
80
+ }
81
+ return value;
82
+ },
83
+ subscribe,
84
+ reaction,
85
+ dispose,
86
+ };
87
+ }
88
+
89
+ export function writable<T>(value: T, start: StartStopNotifier<T> = noop, set: Subscriber<T>): Writable<T> {
90
+ const internal = readable(value, start);
91
+ return {
92
+ get value() {
93
+ return internal.value;
94
+ },
95
+ subscribe: internal.subscribe,
96
+ reaction: internal.reaction,
97
+ set,
98
+ update(fn: Updater<T>) {
99
+ set(fn(internal.value));
100
+ },
101
+ dispose: internal.dispose,
102
+ };
103
+ }