@tiddlywiki/mws 0.0.21 → 0.1.1
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/README.md +42 -9
- package/dist/metafile-esm.json +1 -0
- package/dist/mws.d.ts +1 -0
- package/dist/mws.js +34710 -0
- package/dist/mws.js.map +1 -0
- package/editions/mws-docs/tiddlers/$__StoryList.tid +1 -2
- package/editions/mws-docs/tiddlers/2025-02-18 - User Management.md.meta +2 -2
- package/editions/mws-docs/tiddlers/2025-04-14 - client plugins /342/234/224/357/270/217.md" +31 -0
- package/editions/mws-docs/tiddlers/2025-04-14 - client plugins /342/234/224/357/270/217.md.meta" +5 -0
- package/editions/mws-docs/tiddlers/{2025-05-04.md → 2025-05-04 - event hooks /342/234/224/357/270/217.md" } +7 -1
- package/editions/mws-docs/tiddlers/2025-05-04 - event hooks /342/234/224/357/270/217.md.meta" +5 -0
- package/editions/mws-docs/tiddlers/{2025-05-06.md → 2025-05-06 - syncer design.md } +0 -4
- package/editions/mws-docs/tiddlers/2025-05-06 - syncer design.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Allowed Methods.tid +8 -0
- package/editions/mws-docs/tiddlers/Application Access & Security.md +14 -84
- package/editions/mws-docs/tiddlers/Application Access & Security.md.meta +1 -1
- package/editions/mws-docs/tiddlers/Body Format.md +19 -0
- package/editions/mws-docs/tiddlers/Body Format.md.meta +4 -0
- package/editions/mws-docs/tiddlers/Checklists and Features.md +0 -3
- package/editions/mws-docs/tiddlers/Checklists and Features.md.meta +1 -1
- package/editions/mws-docs/tiddlers/Client Plugins.md +17 -0
- package/editions/mws-docs/tiddlers/Client Plugins.md.meta +5 -0
- package/editions/mws-docs/tiddlers/HTTP API.md +4 -3
- package/editions/mws-docs/tiddlers/HTTP API.md.meta +1 -1
- package/editions/mws-docs/tiddlers/HelloThere.tid +2 -2
- package/editions/mws-docs/tiddlers/Installation.tid +2 -3
- package/editions/mws-docs/tiddlers/MWS Architecture.tid +6 -1
- package/editions/mws-docs/tiddlers/Multi-user Trust.md +12 -0
- package/editions/mws-docs/tiddlers/Multi-user Trust.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Notes.tid +5 -3
- package/editions/mws-docs/tiddlers/Oracle Attacks.md +29 -0
- package/editions/mws-docs/tiddlers/Oracle Attacks.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Reference.md +1 -0
- package/editions/mws-docs/tiddlers/Reference.md.meta +1 -1
- package/editions/mws-docs/tiddlers/Request Handling.md +53 -0
- package/editions/mws-docs/tiddlers/Request Handling.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Roadmap.tid +7 -0
- package/editions/mws-docs/tiddlers/Route Definers.md +206 -0
- package/editions/mws-docs/tiddlers/Route Definers.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Security.md +75 -0
- package/editions/mws-docs/tiddlers/Security.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Server Plugins.md +7 -0
- package/editions/mws-docs/tiddlers/Server Plugins.md.meta +5 -0
- package/editions/mws-docs/tiddlers/Startup Cache.md +28 -0
- package/editions/mws-docs/tiddlers/Startup Cache.md.meta +5 -0
- package/editions/mws-docs/tiddlers/TiddlerFields.md +5 -0
- package/editions/mws-docs/tiddlers/TiddlerFields.md.meta +4 -0
- package/editions/mws-docs/tiddlers/Usage.tid +2 -2
- package/editions/mws-docs/tiddlers/system/$__mws_cssvar-macro.tid +10 -0
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI +143 -0
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI.meta +7 -0
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI_styles.css +567 -0
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI_styles.css.meta +6 -0
- package/editions/mws-docs/tiddlers/system/$__theme.tid +6 -0
- package/editions/mws-docs/tiddlers/system/SplashScreen.tid +21 -0
- package/editions/mws-docs/tiddlers/system/StaticBanner.tid +1 -1
- package/editions/mws-docs/tiddlers/system/Styles.tid +13 -0
- package/editions/mws-docs/tiddlers/system/palette.tid +4 -1
- package/editions/mws-docs/tiddlers/system/sidebarlayout.tid +4 -1
- package/editions/mws-docs/tiddlers/x-mws-tiddler.md +35 -0
- package/editions/mws-docs/tiddlers/x-mws-tiddler.md.meta +5 -0
- package/mws.run.mjs +30 -0
- package/package.json +47 -36
- package/plugins/client/package-lock.json +46 -83
- package/plugins/client/package.json +1 -6
- package/plugins/client/plugin.info +2 -2
- package/plugins/client/src/fflate.d.ts +1501 -0
- package/plugins/client/src/multiwikiclientadaptor.ts +768 -317
- package/plugins/client/src/mws.d.ts +4 -0
- package/plugins/client/src/types.d.ts +3928 -0
- package/plugins/client/tiddlers/en-US.multids +3 -0
- package/plugins/client/tiddlers/status/SaveWikiButtonTemplate.tid +3 -0
- package/plugins/client/tiddlers/status/icon-cloud.tid +4 -0
- package/plugins/client/tiddlers/status/info-segment.tid +8 -0
- package/plugins/client/tiddlers/status/readonly-styles.tid +27 -0
- package/plugins/client/tiddlers/status/save-offline.tid +7 -0
- package/plugins/client/tiddlers/status/save-wiki-button.tid +26 -0
- package/plugins/client/tiddlers/status/styles.tid +44 -0
- package/plugins/client/tiddlers/status/syncer-actions-copy-logs.tid +6 -0
- package/plugins/client/tiddlers/status/syncer-actions-login-status.tid +9 -0
- package/plugins/client/tiddlers/status/syncer-actions-login.tid +8 -0
- package/plugins/client/tiddlers/status/syncer-actions-logout.tid +8 -0
- package/plugins/client/tiddlers/status/syncer-actions-refresh.tid +9 -0
- package/plugins/client/tiddlers/status/syncer-actions-save-snapshot.tid +9 -0
- package/plugins/client/tiddlers/status/tags-syncerdropdown.tid +2 -0
- package/plugins/client/tiddlers/syncer/multiwikiclientadaptor.js +640 -0
- package/plugins/client/tiddlers/syncer/syncer.js +831 -0
- package/plugins/client/tsconfig.json +19 -16
- package/plugins/filter-ast-to-sql/example-run-series.ts +146 -0
- package/plugins/server/SplashScreen.tid +150 -0
- package/plugins/server/fflate.js +9 -0
- package/plugins/server/plugin.info +8 -0
- package/plugins/server/readme +1 -0
- package/plugins/server/url-polyfill.js +294 -0
- package/prisma/client/client.d.ts +1 -0
- package/prisma/client/client.js +3 -0
- package/prisma/client/default.d.ts +1 -0
- package/prisma/client/default.js +3 -0
- package/prisma/client/edge.d.ts +1 -0
- package/prisma/client/edge.js +297 -0
- package/prisma/client/index-browser.js +275 -0
- package/prisma/client/index.d.ts +17770 -0
- package/prisma/client/index.js +324 -0
- package/prisma/client/package.json +173 -0
- package/prisma/client/query_engine_bg.js +811 -0
- package/prisma/client/query_engine_bg.wasm +0 -0
- package/prisma/client/runtime/edge-esm.js +10000 -0
- package/prisma/client/runtime/edge.js +10036 -0
- package/prisma/client/runtime/index-browser.d.ts +370 -0
- package/prisma/client/runtime/index-browser.js +2300 -0
- package/prisma/client/runtime/library.d.ts +3681 -0
- package/prisma/client/runtime/library.js +10604 -0
- package/prisma/client/runtime/react-native.js +10629 -0
- package/prisma/client/runtime/wasm-compiler-edge.js +11238 -0
- package/prisma/client/runtime/wasm-engine-edge.js +7801 -0
- package/prisma/client/schema.prisma +156 -0
- package/prisma/client/wasm-edge-light-loader.mjs +4 -0
- package/prisma/client/wasm-worker-loader.mjs +4 -0
- package/prisma/client/wasm.d.ts +1 -0
- package/prisma/client/wasm.js +304 -0
- package/prisma/global.ts +53 -0
- package/prisma/migrations/{20250406213424_init → 20250606001949_init}/migration.sql +46 -48
- package/prisma/migrations/20250613000526_remove_group/migration.sql +22 -0
- package/prisma/migrations/20250620014502_settings/migration.sql +5 -0
- package/prisma/migrations/20250707205229_recipes/migration.sql +20 -0
- package/prisma/migrations/migration_lock.toml +1 -1
- package/prisma/notes.md +1 -2
- package/prisma/schema.prisma +52 -49
- package/prisma/test.sqlite +0 -0
- package/prisma/test.sqlite-journal +0 -0
- package/public/react-admin/android-chrome-192x192.png +0 -0
- package/public/react-admin/android-chrome-512x512.png +0 -0
- package/public/react-admin/apple-touch-icon.png +0 -0
- package/{react-user-mgmt/public/esm-QKDFEP3B.js → public/react-admin/esm-L4WLVWJN.js} +1 -1
- package/public/react-admin/esm-L4WLVWJN.js.map +7 -0
- package/public/react-admin/esm-QMNGI3LF.js +3 -0
- package/public/react-admin/favicon-16x16.png +0 -0
- package/public/react-admin/favicon-32x32.png +0 -0
- package/public/react-admin/favicon.ico +0 -0
- package/public/react-admin/index.html +36 -0
- package/public/react-admin/main.js +268 -0
- package/public/react-admin/main.js.map +7 -0
- package/public/react-admin/site.webmanifest +20 -0
- package/public/react-admin/stats.json +1 -0
- package/scripts.mjs +134 -25
- package/dist/server.d.ts +0 -945
- package/dist/server.js +0 -20775
- package/dist/server.js.map +0 -1
- package/editions/mws-docs/tiddlers/2025-04-03 - Phases of Development.md +0 -64
- package/editions/mws-docs/tiddlers/2025-04-03 - Phases of Development.md.meta +0 -5
- package/editions/mws-docs/tiddlers/2025-04-13 - Plugins.md +0 -18
- package/editions/mws-docs/tiddlers/2025-04-13 - Plugins.md.meta +0 -5
- package/editions/mws-docs/tiddlers/2025-05-04.md.meta +0 -5
- package/editions/mws-docs/tiddlers/2025-05-06.md.meta +0 -5
- package/editions/mws-docs/tiddlers/Moduler Design.md +0 -29
- package/editions/mws-docs/tiddlers/Moduler Design.md.meta +0 -5
- package/editions/mws-docs/tiddlers/Request Server.md +0 -20
- package/editions/mws-docs/tiddlers/Request Server.md.meta +0 -5
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI-Dark +0 -173
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI-Dark.meta +0 -6
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI-Light +0 -172
- package/editions/mws-docs/tiddlers/system/$__palettes_MUI-Light.meta +0 -6
- package/plugins/client/tiddlers/config-tiddlers-filter.tid +0 -2
- package/plugins/client/tiddlers/config-use-server-sent-events.tid +0 -2
- package/plugins/client/tiddlers/info-segment.tid +0 -8
- package/plugins/client/tiddlers/multiwikiclientadaptor.js +0 -444
- package/prisma/migrations/20250407002306_disconnect_user_tables/migration.sql +0 -52
- package/prisma/old-db.sql +0 -144
- package/react-user-mgmt/README.md +0 -50
- package/react-user-mgmt/eslint.config.js +0 -28
- package/react-user-mgmt/package-lock.json +0 -2738
- package/react-user-mgmt/package.json +0 -31
- package/react-user-mgmt/public/index.html +0 -1
- package/react-user-mgmt/public/main.js +0 -295
- package/react-user-mgmt/public/main.js.map +0 -7
- package/react-user-mgmt/public/stats.json +0 -1
- package/react-user-mgmt/tsconfig.json +0 -40
- package/react-user-mgmt/types.d.ts +0 -2
- package/tiddlywiki-types/boot/boot.d.ts +0 -378
- package/tiddlywiki-types/boot/bootprefix.d.ts +0 -110
- package/tiddlywiki-types/core/modules/filters.d.ts +0 -107
- package/tiddlywiki-types/core/modules/tiddler.d.ts +0 -47
- package/tiddlywiki-types/core/modules/wiki-bulkops.d.ts +0 -36
- package/tiddlywiki-types/core/modules/wiki.d.ts +0 -208
- package/tiddlywiki-types/index.d.ts +0 -7
- package/tiddlywiki-types/package.json +0 -3
- /package/plugins/client/tiddlers/{sidebarsegment.tid → status/sidebarsegment.tid} +0 -0
- /package/plugins/client/tiddlers/{GettingStarted.tid → syncer/GettingStarted.tid} +0 -0
- /package/plugins/client/tiddlers/{config-incoming-updates-filter.tid → syncer/config-incoming-updates-filter.tid} +0 -0
- /package/plugins/client/tiddlers/{readme.tid → syncer/readme.tid} +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/chunk-YALWN5OT.js +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/chunk-YALWN5OT.js.map +0 -0
- /package/{react-user-mgmt/public/esm-QKDFEP3B.js.map → public/react-admin/esm-QMNGI3LF.js.map} +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/favicon.png +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/missing-favicon.png +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/motovun-jack.jpg +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/mws-icon.png +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/stats.css +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/stats.html +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/stats.js +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/stats.svg +0 -0
- /package/{react-user-mgmt/public → public/react-admin}/vite.svg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*\
|
|
2
|
-
title: $:/plugins/
|
|
2
|
+
title: $:/plugins/mws/client/tiddlywebadaptor.js
|
|
3
3
|
type: application/javascript
|
|
4
4
|
module-type: syncadaptor
|
|
5
5
|
|
|
@@ -18,23 +18,192 @@ previous operation to complete before sending a new one.
|
|
|
18
18
|
|
|
19
19
|
// the blank line is important, and so is the following use strict
|
|
20
20
|
"use strict";
|
|
21
|
-
|
|
22
|
-
import type {
|
|
21
|
+
|
|
22
|
+
import type { ServerEventsMap } from '@tiddlywiki/events';
|
|
23
|
+
import type { ZodRoute, WikiStatusRoutes, WikiRecipeRoutes } from '@tiddlywiki/mws';
|
|
24
|
+
import type { zod } from '@tiddlywiki/server';
|
|
25
|
+
import type { Syncer, Tiddler, TiddlerFields } from 'tiddlywiki';
|
|
26
|
+
declare global { const fflate: typeof import("./fflate"); }
|
|
27
|
+
declare const self: never;
|
|
28
|
+
|
|
29
|
+
// registerZodRoutes(parent, new WikiStatusRoutes(), Object.keys({
|
|
30
|
+
// handleGetAllBagStates: true,
|
|
31
|
+
// handleGetBagState: true,
|
|
32
|
+
// handleGetRecipeStatus: true,
|
|
33
|
+
// handleGetRecipeEvents: true,
|
|
34
|
+
// handleGetBags: true,
|
|
35
|
+
// } satisfies RouterKeyMap<WikiStatusRoutes, true>));
|
|
36
|
+
|
|
37
|
+
// registerZodRoutes(parent, new WikiRecipeRoutes(), Object.keys({
|
|
38
|
+
// handleDeleteRecipeTiddler: true,
|
|
39
|
+
// handleLoadRecipeTiddler: true,
|
|
40
|
+
// handleSaveRecipeTiddler: true,
|
|
41
|
+
// rpcDeleteRecipeTiddlerList: true,
|
|
42
|
+
// rpcLoadRecipeTiddlerList: true,
|
|
43
|
+
// rpcSaveRecipeTiddlerList: true,
|
|
44
|
+
// } satisfies RouterKeyMap<WikiRecipeRoutes, true>));
|
|
45
|
+
|
|
46
|
+
declare class Logger {
|
|
47
|
+
constructor(componentName: any, options: any);
|
|
48
|
+
componentName: any;
|
|
49
|
+
colour: any;
|
|
50
|
+
enable: any;
|
|
51
|
+
save: any;
|
|
52
|
+
saveLimit: any;
|
|
53
|
+
saveBufferLogger: this;
|
|
54
|
+
buffer: string;
|
|
55
|
+
alertCount: number;
|
|
56
|
+
setSaveBuffer(logger: any): void;
|
|
57
|
+
log(...args: any[]): any;
|
|
58
|
+
getBuffer(): string;
|
|
59
|
+
table(value: any): void;
|
|
60
|
+
alert(...args: any[]): void;
|
|
61
|
+
clearAlerts(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type WikiStatusTypes = {
|
|
65
|
+
[K in keyof WikiStatusRoutes]: WikiStatusRoutes[K] extends ZodRoute<infer M, infer B, infer P, infer Q, infer T, infer R>
|
|
66
|
+
? { M: M, B: B, P: P, Q: Q, T: T, R: R }
|
|
67
|
+
: never
|
|
68
|
+
}
|
|
69
|
+
type WikiRecipeTypes = {
|
|
70
|
+
[K in keyof WikiRecipeRoutes]: WikiRecipeRoutes[K] extends ZodRoute<infer M, infer B, infer P, infer Q, infer T, infer R>
|
|
71
|
+
? { M: M, B: B, P: P, Q: Q, T: T, R: R }
|
|
72
|
+
: never
|
|
73
|
+
}
|
|
74
|
+
interface RouteTypes extends WikiStatusTypes, WikiRecipeTypes {
|
|
75
|
+
}
|
|
23
76
|
|
|
24
77
|
declare module 'tiddlywiki' {
|
|
25
|
-
export interface Syncer {
|
|
78
|
+
export interface Syncer<AD> {
|
|
26
79
|
wiki: Wiki;
|
|
27
80
|
logger: Logger;
|
|
28
|
-
tiddlerInfo: Record<string, {
|
|
81
|
+
tiddlerInfo: Record<string, {
|
|
82
|
+
changeCount: number,
|
|
83
|
+
adaptorInfo: AD,
|
|
84
|
+
revision: string,
|
|
85
|
+
timestampLastSaved: Date
|
|
86
|
+
}>;
|
|
29
87
|
enqueueLoadTiddler(title: string): void;
|
|
30
88
|
storeTiddler(tiddler: Tiddler): void;
|
|
31
89
|
processTaskQueue(): void;
|
|
90
|
+
syncFromServer(): void;
|
|
32
91
|
}
|
|
33
92
|
interface ITiddlyWiki {
|
|
34
93
|
browserStorage: any;
|
|
35
94
|
}
|
|
36
95
|
}
|
|
37
96
|
|
|
97
|
+
type ServerStatusCallback = (
|
|
98
|
+
err: any,
|
|
99
|
+
/**
|
|
100
|
+
* $:/status/IsLoggedIn mostly appears alongside the username
|
|
101
|
+
* or other login-conditional behavior.
|
|
102
|
+
*/
|
|
103
|
+
isLoggedIn?: boolean,
|
|
104
|
+
/**
|
|
105
|
+
* $:/status/UserName is still used for things like drafts even if the
|
|
106
|
+
* user isn't logged in, although the username is less likely to be shown
|
|
107
|
+
* to the user.
|
|
108
|
+
*/
|
|
109
|
+
username?: string,
|
|
110
|
+
/**
|
|
111
|
+
* $:/status/IsReadOnly puts the UI in readonly mode,
|
|
112
|
+
* but does not prevent automatic changes from attempting to save.
|
|
113
|
+
*/
|
|
114
|
+
isReadOnly?: boolean,
|
|
115
|
+
/**
|
|
116
|
+
* $:/status/IsAnonymous does not appear anywhere in the TW5 repo!
|
|
117
|
+
* So it has no apparent purpose.
|
|
118
|
+
*/
|
|
119
|
+
isAnonymous?: boolean
|
|
120
|
+
) => void
|
|
121
|
+
|
|
122
|
+
interface SyncAdaptor<AD> {
|
|
123
|
+
name?: string;
|
|
124
|
+
|
|
125
|
+
isReady?(): boolean;
|
|
126
|
+
|
|
127
|
+
getStatus?(
|
|
128
|
+
cb: ServerStatusCallback
|
|
129
|
+
): void;
|
|
130
|
+
|
|
131
|
+
getSkinnyTiddlers?(
|
|
132
|
+
cb: (err: any, tiddlerFields: Record<string, string>[]) => void
|
|
133
|
+
): void;
|
|
134
|
+
getUpdatedTiddlers?(
|
|
135
|
+
syncer: Syncer<AD>,
|
|
136
|
+
cb: (
|
|
137
|
+
err: any,
|
|
138
|
+
/** Arrays of titles that have been modified or deleted */
|
|
139
|
+
updates?: { modifications: string[], deletions: string[] }
|
|
140
|
+
) => void
|
|
141
|
+
): void;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* used to override the default Syncer getTiddlerRevision behavior
|
|
145
|
+
* of returning the revision field
|
|
146
|
+
*
|
|
147
|
+
*/
|
|
148
|
+
getTiddlerRevision?(title: string): string;
|
|
149
|
+
/**
|
|
150
|
+
* used to get the adapter info from a tiddler in situations
|
|
151
|
+
* other than the saveTiddler callback
|
|
152
|
+
*/
|
|
153
|
+
getTiddlerInfo(tiddler: Tiddler): AD | undefined;
|
|
154
|
+
|
|
155
|
+
saveTiddler(
|
|
156
|
+
tiddler: any,
|
|
157
|
+
cb: (
|
|
158
|
+
err: any,
|
|
159
|
+
adaptorInfo?: AD,
|
|
160
|
+
revision?: string
|
|
161
|
+
) => void,
|
|
162
|
+
extra: { tiddlerInfo: SyncerTiddlerInfo<AD> }
|
|
163
|
+
): void;
|
|
164
|
+
|
|
165
|
+
saveTiddlers?(options: {
|
|
166
|
+
syncer: Syncer<AD>,
|
|
167
|
+
tiddlers: Tiddler[],
|
|
168
|
+
onNext: (title: string, adaptorInfo: any, revision: string) => void,
|
|
169
|
+
onDone: () => void,
|
|
170
|
+
onError: (err: Error) => void
|
|
171
|
+
}): void;
|
|
172
|
+
|
|
173
|
+
loadTiddlers?(options: {
|
|
174
|
+
syncer: Syncer<AD>,
|
|
175
|
+
titles: string[],
|
|
176
|
+
onNext: (tiddlerFields: TiddlerFields) => void,
|
|
177
|
+
onDone: () => void,
|
|
178
|
+
onError: (err: Error) => void
|
|
179
|
+
}): void;
|
|
180
|
+
|
|
181
|
+
deleteTiddlers?(options: {
|
|
182
|
+
syncer: Syncer<AD>,
|
|
183
|
+
titles: string[],
|
|
184
|
+
onNext: (title: string) => void,
|
|
185
|
+
onDone: () => void,
|
|
186
|
+
onError: (err: Error) => void
|
|
187
|
+
}): void;
|
|
188
|
+
|
|
189
|
+
setLoggerSaveBuffer?: (loggerForSaving: Logger) => void;
|
|
190
|
+
displayLoginPrompt?(syncer: Syncer<AD>): void;
|
|
191
|
+
login?(username: string, password: string, cb: (err: any) => void): void;
|
|
192
|
+
logout?(cb: (err: any) => void): any;
|
|
193
|
+
}
|
|
194
|
+
interface SyncerTiddlerInfo<AD> {
|
|
195
|
+
/** this comes from the wiki changeCount record */
|
|
196
|
+
changeCount: number;
|
|
197
|
+
/** Adapter info returned by the sync adapter */
|
|
198
|
+
adaptorInfo: AD;
|
|
199
|
+
/** Revision return by the sync adapter */
|
|
200
|
+
revision: string;
|
|
201
|
+
/** Timestamp set in the callback of the previous save */
|
|
202
|
+
timestampLastSaved: Date;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
declare const $tw: any;
|
|
206
|
+
|
|
38
207
|
declare const exports: {
|
|
39
208
|
adaptorClass: typeof MultiWikiClientAdaptor;
|
|
40
209
|
};
|
|
@@ -46,48 +215,65 @@ var CONFIG_HOST_TIDDLER = "$:/config/multiwikiclient/host",
|
|
|
46
215
|
REVISION_STATE_TIDDLER = "$:/state/multiwikiclient/tiddlers/revision",
|
|
47
216
|
CONNECTION_STATE_TIDDLER = "$:/state/multiwikiclient/connection",
|
|
48
217
|
INCOMING_UPDATES_FILTER_TIDDLER = "$:/config/multiwikiclient/incoming-updates-filter",
|
|
49
|
-
ENABLE_SSE_TIDDLER = "$:/config/multiwikiclient/use-server-sent-events"
|
|
218
|
+
ENABLE_SSE_TIDDLER = "$:/config/multiwikiclient/use-server-sent-events",
|
|
219
|
+
IS_DEV_MODE_TIDDLER = `$:/state/multiwikiclient/dev-mode`,
|
|
220
|
+
ENABLE_GZIP_STREAM_TIDDLER = `$:/state/multiwikiclient/gzip-stream`;
|
|
221
|
+
|
|
50
222
|
|
|
51
223
|
var SERVER_NOT_CONNECTED = "NOT CONNECTED",
|
|
52
224
|
SERVER_CONNECTING_SSE = "CONNECTING SSE",
|
|
53
225
|
SERVER_CONNECTED_SSE = "CONNECTED SSE",
|
|
54
226
|
SERVER_POLLING = "SERVER POLLING";
|
|
55
227
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
228
|
+
interface MWSAdaptorInfo {
|
|
229
|
+
bag: string
|
|
230
|
+
revision: string
|
|
231
|
+
title: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class MultiWikiClientAdaptor implements SyncAdaptor<MWSAdaptorInfo> {
|
|
236
|
+
private wiki;
|
|
237
|
+
private host;
|
|
238
|
+
private recipe;
|
|
239
|
+
private useServerSentEvents;
|
|
240
|
+
private last_known_revision_id;
|
|
241
|
+
private outstandingRequests;
|
|
242
|
+
private lastRecordedUpdate;
|
|
243
|
+
private logger;
|
|
244
|
+
private isLoggedIn;
|
|
245
|
+
private isReadOnly;
|
|
246
|
+
private offline;
|
|
247
|
+
private username;
|
|
248
|
+
private incomingUpdatesFilterFn;
|
|
249
|
+
private serverUpdateConnectionStatus!: string;
|
|
250
|
+
private isDevMode: boolean;
|
|
251
|
+
private useGzipStream: boolean;
|
|
252
|
+
private triggerPoll: (() => void) | null = null;
|
|
70
253
|
|
|
71
254
|
name = "multiwikiclient";
|
|
72
|
-
supportsLazyLoading = true;
|
|
255
|
+
private supportsLazyLoading = true;
|
|
73
256
|
constructor(options: { wiki: any }) {
|
|
74
257
|
this.wiki = options.wiki;
|
|
75
258
|
this.host = this.getHost();
|
|
76
259
|
this.recipe = this.wiki.getTiddlerText("$:/config/multiwikiclient/recipe");
|
|
77
260
|
this.useServerSentEvents = this.wiki.getTiddlerText(ENABLE_SSE_TIDDLER) === "yes";
|
|
78
|
-
this.
|
|
261
|
+
this.isDevMode = this.wiki.getTiddlerText(IS_DEV_MODE_TIDDLER) === "yes";
|
|
262
|
+
this.useGzipStream = this.wiki.getTiddlerText(ENABLE_GZIP_STREAM_TIDDLER) === "yes";
|
|
263
|
+
this.last_known_revision_id = this.wiki.getTiddlerText("$:/state/multiwikiclient/recipe/last_revision_id", "0")
|
|
79
264
|
this.outstandingRequests = Object.create(null); // Hashmap by title of outstanding request object: {type: "PUT"|"GET"|"DELETE"}
|
|
80
|
-
this.lastRecordedUpdate = Object.create(null); // Hashmap by title of last recorded update via SSE: {type: "update"|"detetion",
|
|
265
|
+
this.lastRecordedUpdate = Object.create(null); // Hashmap by title of last recorded update via SSE: {type: "update"|"detetion", revision_id:}
|
|
81
266
|
this.logger = new $tw.utils.Logger("MultiWikiClientAdaptor");
|
|
82
267
|
this.isLoggedIn = false;
|
|
83
268
|
this.isReadOnly = false;
|
|
84
|
-
this.
|
|
269
|
+
this.offline = false;
|
|
270
|
+
this.username = "";
|
|
85
271
|
// Compile the dirty tiddler filter
|
|
86
272
|
this.incomingUpdatesFilterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(INCOMING_UPDATES_FILTER_TIDDLER));
|
|
87
|
-
this.
|
|
273
|
+
this.setConnectionStatus(SERVER_NOT_CONNECTED);
|
|
88
274
|
}
|
|
89
275
|
|
|
90
|
-
|
|
276
|
+
private setConnectionStatus(status: string) {
|
|
91
277
|
this.serverUpdateConnectionStatus = status;
|
|
92
278
|
this.wiki.addTiddler({
|
|
93
279
|
title: CONNECTION_STATE_TIDDLER,
|
|
@@ -100,20 +286,29 @@ class MultiWikiClientAdaptor {
|
|
|
100
286
|
isReady() {
|
|
101
287
|
return true;
|
|
102
288
|
}
|
|
103
|
-
|
|
289
|
+
subscribe(callback: () => void) {
|
|
290
|
+
// TODO: not working at the moment for some reason
|
|
291
|
+
console.log("Subscribing to server updates");
|
|
292
|
+
this.triggerPoll = callback;
|
|
293
|
+
this.connectRecipeEvents();
|
|
294
|
+
}
|
|
295
|
+
private getHost() {
|
|
104
296
|
var text = this.wiki.getTiddlerText(CONFIG_HOST_TIDDLER, DEFAULT_HOST_TIDDLER), substitutions = [
|
|
105
297
|
{ name: "protocol", value: document.location.protocol },
|
|
106
298
|
{ name: "host", value: document.location.host },
|
|
107
299
|
{ name: "pathname", value: document.location.pathname }
|
|
108
300
|
];
|
|
109
301
|
for (var t = 0; t < substitutions.length; t++) {
|
|
110
|
-
var s = substitutions[t]
|
|
302
|
+
var s = substitutions[t]!;
|
|
111
303
|
text = $tw.utils.replaceString(text, new RegExp("\\$" + s.name + "\\$", "mg"), s.value);
|
|
112
304
|
}
|
|
113
305
|
return text;
|
|
114
306
|
}
|
|
115
|
-
|
|
116
|
-
|
|
307
|
+
|
|
308
|
+
getTiddlerInfo(tiddler: Tiddler): MWSAdaptorInfo | undefined {
|
|
309
|
+
var title = tiddler.fields.title,
|
|
310
|
+
revision = this.wiki.extractTiddlerDataItem(REVISION_STATE_TIDDLER, title),
|
|
311
|
+
bag = this.wiki.extractTiddlerDataItem(BAG_STATE_TIDDLER, title);
|
|
117
312
|
if (revision && bag) {
|
|
118
313
|
return {
|
|
119
314
|
title: title,
|
|
@@ -124,354 +319,374 @@ class MultiWikiClientAdaptor {
|
|
|
124
319
|
return undefined;
|
|
125
320
|
}
|
|
126
321
|
}
|
|
127
|
-
getTiddlerBag(title: string) {
|
|
322
|
+
private getTiddlerBag(title: string) {
|
|
128
323
|
return this.wiki.extractTiddlerDataItem(BAG_STATE_TIDDLER, title);
|
|
129
324
|
}
|
|
130
325
|
getTiddlerRevision(title: string) {
|
|
131
326
|
return this.wiki.extractTiddlerDataItem(REVISION_STATE_TIDDLER, title);
|
|
132
327
|
}
|
|
133
|
-
setTiddlerInfo(title: string
|
|
134
|
-
this.wiki.setText(BAG_STATE_TIDDLER, null, title,
|
|
135
|
-
this.wiki.setText(REVISION_STATE_TIDDLER, null, title,
|
|
328
|
+
private setTiddlerInfo({ title, revision_id, bag_name }: { title: string; revision_id: string; bag_name: string; }) {
|
|
329
|
+
this.wiki.setText(BAG_STATE_TIDDLER, null, title, bag_name, { suppressTimestamp: true });
|
|
330
|
+
this.wiki.setText(REVISION_STATE_TIDDLER, null, title, revision_id, { suppressTimestamp: true });
|
|
136
331
|
}
|
|
137
|
-
removeTiddlerInfo(title: string) {
|
|
332
|
+
private removeTiddlerInfo({ title, revision_id, bag_id }: { title: string; revision_id: string; bag_id: string; }) {
|
|
138
333
|
this.wiki.setText(BAG_STATE_TIDDLER, null, title, undefined, { suppressTimestamp: true });
|
|
139
334
|
this.wiki.setText(REVISION_STATE_TIDDLER, null, title, undefined, { suppressTimestamp: true });
|
|
140
335
|
}
|
|
141
336
|
|
|
142
|
-
|
|
143
|
-
/** url to retrieve (must not contain `?` if GET or HEAD) */
|
|
144
|
-
url: string;
|
|
145
|
-
/** hashmap of headers to send */
|
|
146
|
-
headers?: Record<string, string>;
|
|
147
|
-
/** request method: GET, PUT, POST etc */
|
|
148
|
-
type?: string;
|
|
149
|
-
/** optional function invoked with (lengthComputable,loaded,total) */
|
|
150
|
-
progress?: (lengthComputable: boolean, loaded: number, total: number) => void;
|
|
151
|
-
/** name of the property to return as first argument of callback */
|
|
152
|
-
returnProp?: string;
|
|
153
|
-
responseType?: RT;
|
|
154
|
-
useDefaultHeaders?: boolean;
|
|
155
|
-
/** urlencoded string or hashmap of data to send. If type is GET or HEAD, this is appended to the URL as a query string */
|
|
156
|
-
data?: object | string;
|
|
157
|
-
}) {
|
|
158
|
-
type ResponseErr = [false, any, undefined];
|
|
159
|
-
type ResponseOk = [true, undefined, {
|
|
160
|
-
data:
|
|
161
|
-
"json" extends RT ? any :
|
|
162
|
-
"text" extends RT ? string :
|
|
163
|
-
"arraybuffer" extends RT ? ArrayBuffer :
|
|
164
|
-
unknown;
|
|
165
|
-
headers: Record<string, string>
|
|
166
|
-
}];
|
|
167
|
-
return (new Promise<ResponseErr | ResponseOk>((resolve) => {
|
|
168
|
-
$tw.utils.httpRequest({
|
|
169
|
-
...options,
|
|
170
|
-
responseType: options.responseType === "json" ? "text" : options.responseType,
|
|
171
|
-
callback: (err: any, data: any, request: XMLHttpRequest) => {
|
|
172
|
-
if (err) return resolve([false, err || new Error("Unknown error"), undefined]);
|
|
173
|
-
|
|
174
|
-
// Create a map of header names to values
|
|
175
|
-
|
|
176
|
-
const headers = {} as any;
|
|
177
|
-
request.getAllResponseHeaders()?.trim().split(/[\r\n]+/).forEach((line) => {
|
|
178
|
-
const parts = line.split(": ");
|
|
179
|
-
const header = parts.shift()?.toLowerCase();
|
|
180
|
-
const value = parts.join(": ");
|
|
181
|
-
if (header) headers[header] = value;
|
|
182
|
-
});
|
|
183
|
-
// Resolve the promise with the response data and headers
|
|
184
|
-
resolve([true, undefined, {
|
|
185
|
-
headers,
|
|
186
|
-
data: options.responseType === "json" ? $tw.utils.parseJSONSafe(data, () => undefined) : data,
|
|
187
|
-
}]);
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}));
|
|
191
|
-
}
|
|
337
|
+
error: string | null = null;
|
|
192
338
|
/*
|
|
193
339
|
Get the current status of the server connection
|
|
194
340
|
*/
|
|
195
|
-
async getStatus(callback:
|
|
196
|
-
err: any,
|
|
197
|
-
isLoggedIn?: boolean,
|
|
198
|
-
username?: string,
|
|
199
|
-
isReadOnly?: boolean,
|
|
200
|
-
isAnonymous?: boolean,
|
|
201
|
-
) => void) {
|
|
202
|
-
interface UserAuthStatus {
|
|
203
|
-
isAdmin: boolean;
|
|
204
|
-
user_id: number;
|
|
205
|
-
username: string;
|
|
206
|
-
isLoggedIn: boolean;
|
|
207
|
-
isReadOnly: boolean;
|
|
208
|
-
allowAnonReads: boolean;
|
|
209
|
-
allowAnonWrites: boolean;
|
|
210
|
-
}
|
|
341
|
+
async getStatus(callback: ServerStatusCallback) {
|
|
211
342
|
|
|
212
|
-
const [ok, error, data] = await this.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
headers: {
|
|
217
|
-
'Content-Type': 'application/json',
|
|
218
|
-
"X-Requested-With": "TiddlyWiki"
|
|
219
|
-
},
|
|
343
|
+
const [ok, error, data] = await this.recipeRequest({
|
|
344
|
+
key: "handleGetRecipeStatus",
|
|
345
|
+
method: "GET",
|
|
346
|
+
url: "/status",
|
|
220
347
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
348
|
+
|
|
349
|
+
if (!ok && data?.status === 0) {
|
|
350
|
+
this.error = "The webpage is forbidden from contacting the server."
|
|
351
|
+
this.isLoggedIn = false;
|
|
352
|
+
this.isReadOnly = true;
|
|
353
|
+
this.username = "(offline)";
|
|
354
|
+
this.offline = true;
|
|
355
|
+
} else if (ok) {
|
|
356
|
+
this.error = null;
|
|
357
|
+
const status = data.responseJSON;
|
|
358
|
+
this.isReadOnly = status?.isReadOnly ?? true;
|
|
359
|
+
this.isLoggedIn = status?.isLoggedIn ?? false;
|
|
360
|
+
this.username = status?.username ?? "(anon)";
|
|
361
|
+
this.offline = false;
|
|
362
|
+
} else {
|
|
363
|
+
this.error = error.message ?? `${error}`;
|
|
225
364
|
}
|
|
226
|
-
/** @type {Partial<UserAuthStatus>} */
|
|
227
|
-
const status = data.data;
|
|
228
365
|
if (callback) {
|
|
229
366
|
callback(
|
|
230
367
|
// Error
|
|
231
|
-
|
|
368
|
+
this.error,
|
|
232
369
|
// Is logged in
|
|
233
|
-
|
|
370
|
+
this.isLoggedIn,
|
|
234
371
|
// Username
|
|
235
|
-
|
|
372
|
+
this.username,
|
|
236
373
|
// Is read only
|
|
237
|
-
|
|
374
|
+
this.isReadOnly,
|
|
238
375
|
// Is anonymous
|
|
239
|
-
|
|
376
|
+
// no idea what this means, always return false
|
|
377
|
+
false,
|
|
240
378
|
);
|
|
241
379
|
}
|
|
242
380
|
}
|
|
243
381
|
/*
|
|
244
382
|
Get details of changed tiddlers from the server
|
|
245
383
|
*/
|
|
246
|
-
getUpdatedTiddlers(syncer: Syncer
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
384
|
+
async getUpdatedTiddlers(syncer: Syncer<MWSAdaptorInfo>, callback: (err: any, changes?: { modifications: string[]; deletions: string[] }) => void) {
|
|
385
|
+
if (this.offline) return callback(null);
|
|
386
|
+
if (!this.bags || this.serverUpdateConnectionStatus === SERVER_NOT_CONNECTED) await this.pollServer();
|
|
387
|
+
const { modifications, deletions, last_revision } = this.updateBags();
|
|
388
|
+
this.last_known_revision_id = last_revision;
|
|
389
|
+
callback(null, { modifications, deletions });
|
|
390
|
+
}
|
|
255
391
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
392
|
+
|
|
393
|
+
connectRecipeEvents() {
|
|
394
|
+
const event = new EventSource(this.host + "recipe/" + encodeURIComponent(this.recipe) + "/events?first-event-id=" + encodeURIComponent(this.last_known_revision_id));
|
|
395
|
+
this.setConnectionStatus(SERVER_CONNECTING_SSE);
|
|
396
|
+
let syncTimeout: NodeJS.Timeout | null = null;
|
|
397
|
+
const debounceSyncFromServer = () => {
|
|
398
|
+
if (syncTimeout) {
|
|
399
|
+
clearTimeout(syncTimeout);
|
|
400
|
+
}
|
|
401
|
+
syncTimeout = setTimeout(() => {
|
|
402
|
+
syncTimeout = null;
|
|
403
|
+
this.triggerPoll?.();
|
|
404
|
+
}, 100);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const onUpdate = (e: MessageEvent) => {
|
|
408
|
+
if (!this.bags) return console.log("No bags loaded, cannot process updates");
|
|
409
|
+
const updates: {
|
|
410
|
+
bag: {
|
|
411
|
+
bag_name: PrismaField<"Bags", "bag_name">;
|
|
412
|
+
tiddlers: {
|
|
413
|
+
revision_id: PrismaField<"Tiddlers", "revision_id">;
|
|
414
|
+
title: PrismaField<"Tiddlers", "title">;
|
|
415
|
+
is_deleted: PrismaField<"Tiddlers", "is_deleted">;
|
|
416
|
+
}[];
|
|
417
|
+
};
|
|
418
|
+
}[] = JSON.parse(e.data);
|
|
419
|
+
const existBags = new Map(this.bags.map(b => [b.bag_name, b.tiddlers] as const));
|
|
420
|
+
updates.forEach(e => {
|
|
421
|
+
const existBag = existBags.get(e.bag.bag_name);
|
|
422
|
+
if (!existBag) return;
|
|
423
|
+
const existTids = new Map(existBag.map(t => [t.title, t] as const));
|
|
424
|
+
e.bag.tiddlers.forEach(tid => {
|
|
425
|
+
const existTid = existTids.get(tid.title);
|
|
426
|
+
if (existTid) {
|
|
427
|
+
Object.assign(existTid, tid)
|
|
428
|
+
} else {
|
|
429
|
+
existBag.push(tid);
|
|
276
430
|
}
|
|
277
431
|
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
432
|
+
});
|
|
433
|
+
debounceSyncFromServer();
|
|
434
|
+
};
|
|
435
|
+
// event.addEventListener("tiddler.save", onEvent);
|
|
436
|
+
// event.addEventListener("tiddler.delete", onEvent);
|
|
437
|
+
event.addEventListener("tiddler.since-last-event", onUpdate);
|
|
438
|
+
event.addEventListener("test", console.log);
|
|
439
|
+
event.addEventListener("open", () => {
|
|
440
|
+
this.setConnectionStatus(SERVER_CONNECTED_SSE);
|
|
441
|
+
console.log("Connected to server events");
|
|
442
|
+
});
|
|
443
|
+
event.addEventListener("error", (e) => {
|
|
444
|
+
if (event.readyState === EventSource.CLOSED) {
|
|
445
|
+
this.setConnectionStatus(SERVER_NOT_CONNECTED);
|
|
446
|
+
console.error("Server events connection closed");
|
|
447
|
+
} else if (event.readyState === EventSource.CONNECTING) {
|
|
448
|
+
this.setConnectionStatus(SERVER_CONNECTING_SSE);
|
|
449
|
+
console.warn("Connecting to server events");
|
|
450
|
+
} else {
|
|
451
|
+
console.error("Error in server events connection", e);
|
|
286
452
|
}
|
|
287
453
|
});
|
|
288
|
-
|
|
289
454
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
onerror: invoked if there is an error
|
|
296
|
-
*/
|
|
297
|
-
connectServerStream(options: {
|
|
298
|
-
syncer: Syncer;
|
|
299
|
-
onopen: (event: Event) => void;
|
|
300
|
-
onerror: (event: Event) => void;
|
|
301
|
-
}) {
|
|
302
|
-
var self = this;
|
|
303
|
-
const eventSource = new EventSource("/recipes/" + this.recipe + "/events?last_known_tiddler_id=" + this.last_known_tiddler_id);
|
|
304
|
-
eventSource.onerror = function (event) {
|
|
305
|
-
if (options.onerror) {
|
|
306
|
-
options.onerror(event);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
eventSource.onopen = function (event) {
|
|
310
|
-
if (options.onopen) {
|
|
311
|
-
options.onopen(event);
|
|
312
|
-
}
|
|
455
|
+
|
|
456
|
+
bags?: {
|
|
457
|
+
position: number & {
|
|
458
|
+
__prisma_table: "Recipe_bags";
|
|
459
|
+
__prisma_field: "position";
|
|
313
460
|
};
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
461
|
+
bag_id: string;
|
|
462
|
+
bag_name: string;
|
|
463
|
+
tiddlers: {
|
|
464
|
+
title: string;
|
|
465
|
+
revision_id: string;
|
|
466
|
+
is_deleted: boolean;
|
|
467
|
+
}[];
|
|
468
|
+
}[]
|
|
469
|
+
|
|
470
|
+
private async pollServer() {
|
|
471
|
+
type t1 = RouteTypes["handleGetBags"];
|
|
472
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
473
|
+
key: "handleGetBags",
|
|
474
|
+
url: "/bags",
|
|
475
|
+
method: "GET",
|
|
476
|
+
});
|
|
477
|
+
if (!ok) throw err;
|
|
478
|
+
if (!result.responseJSON) throw new Error("no result returned");
|
|
479
|
+
type t2 = RouteTypes["handleGetBagState"];
|
|
480
|
+
this.bags = await Promise.all(result.responseJSON.map(e => this.recipeRequest({
|
|
481
|
+
key: "handleGetBagState",
|
|
482
|
+
url: "/bags/" + encodeURIComponent(e.bag_name) + "/state",
|
|
483
|
+
method: "GET",
|
|
484
|
+
queryParams: { include_deleted: "yes" },
|
|
485
|
+
}).then(([ok, err, f]) => {
|
|
486
|
+
if (!ok) throw err;
|
|
487
|
+
if (!f.responseJSON) throw new Error("no result returned");
|
|
488
|
+
return {
|
|
489
|
+
...f.responseJSON,
|
|
490
|
+
position: e.position,
|
|
334
491
|
};
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (data.is_deleted) {
|
|
339
|
-
self.removeTiddlerInfo(data.title);
|
|
340
|
-
delete options.syncer.tiddlerInfo[data.title];
|
|
341
|
-
options.syncer.logger.log("Deleting tiddler missing from server:", data.title);
|
|
342
|
-
options.syncer.wiki.deleteTiddler(data.title);
|
|
343
|
-
options.syncer.processTaskQueue();
|
|
344
|
-
} else {
|
|
345
|
-
var result = self.incomingUpdatesFilterFn.call(self.wiki, self.wiki.makeTiddlerIterator([data.title]));
|
|
346
|
-
if (result.length > 0) {
|
|
347
|
-
self.setTiddlerInfo(data.title, data.tiddler_id.toString(), data.bag_name);
|
|
348
|
-
options.syncer.storeTiddler(data.tiddler);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
492
|
+
})));
|
|
493
|
+
|
|
494
|
+
this.bags.sort((a, b) => b.position - a.position);
|
|
351
495
|
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
updateBags() {
|
|
499
|
+
|
|
500
|
+
if (!this.bags) throw new Error("No bags loaded, call pollServer first");
|
|
501
|
+
|
|
502
|
+
const modified = new Set<string>(),
|
|
503
|
+
deleted = new Set<string>();
|
|
504
|
+
|
|
505
|
+
const incomingTitles = new Set<string>(this.bags.map(
|
|
506
|
+
// get the titles in each layer that aren't deleted
|
|
507
|
+
e => e.tiddlers.filter(f => !f.is_deleted).map(f => f.title)
|
|
508
|
+
// and flatten them for Set
|
|
509
|
+
).flat());
|
|
510
|
+
|
|
511
|
+
let last_revision = this.last_known_revision_id;
|
|
352
512
|
|
|
513
|
+
this.bags.forEach(bag => {
|
|
514
|
+
bag.tiddlers.forEach(tid => {
|
|
515
|
+
// if the revision is old, ignore, since deletions create a new revision
|
|
516
|
+
if (tid.revision_id <= this.last_known_revision_id) return;
|
|
517
|
+
if (tid.revision_id > last_revision) last_revision = tid.revision_id;
|
|
518
|
+
// check if this title still exists in any layer
|
|
519
|
+
if (incomingTitles.has(tid.title))
|
|
520
|
+
modified.add(tid.title);
|
|
521
|
+
else
|
|
522
|
+
deleted.add(tid.title);
|
|
523
|
+
})
|
|
353
524
|
});
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
modifications: [...modified.keys()],
|
|
528
|
+
deletions: [...deleted.keys()],
|
|
529
|
+
last_revision,
|
|
530
|
+
incomingTitles,
|
|
531
|
+
}
|
|
354
532
|
}
|
|
355
533
|
/*
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
callback: invoked on completion as (err,changes)
|
|
534
|
+
Queue a load for a tiddler if there has been an update for it since the specified revision
|
|
359
535
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
536
|
+
private checkLastRecordedUpdate(title: string, revision: string) {
|
|
537
|
+
// var lru = this.lastRecordedUpdate[title];
|
|
538
|
+
// if (lru && lru.revision_id > revision) {
|
|
539
|
+
// console.log(`Checking for updates to ${title} since ${JSON.stringify(lru)} comparing to ${revision}`);
|
|
540
|
+
// this.syncer && this.syncer.enqueueLoadTiddler(title);
|
|
541
|
+
// }
|
|
542
|
+
}
|
|
543
|
+
private get syncer() {
|
|
544
|
+
if ($tw.syncadaptor === this) return $tw.syncer as Syncer<MWSAdaptorInfo>;
|
|
545
|
+
}
|
|
546
|
+
async loadTiddlers(options: {
|
|
547
|
+
syncer: Syncer<MWSAdaptorInfo>;
|
|
548
|
+
titles: string[];
|
|
549
|
+
onNext: (tiddlerFields: TiddlerFields) => void;
|
|
550
|
+
onDone: () => void;
|
|
551
|
+
onError: (err: Error) => void;
|
|
362
552
|
}) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
553
|
+
const { syncer, titles, onNext, onDone, onError } = options;
|
|
554
|
+
|
|
555
|
+
const tiddlers = await this.rpcRequest({
|
|
556
|
+
key: "rpcLoadRecipeTiddlerList",
|
|
557
|
+
body: { titles: titles as PrismaField<"Tiddlers", "title">[] }
|
|
558
|
+
}).catch(err => {
|
|
559
|
+
onError(err);
|
|
560
|
+
throw new Error("Error loading tiddlers: " + err.message);
|
|
561
|
+
});
|
|
562
|
+
tiddlers.forEach(({ bag_name, tiddler: fields }) => {
|
|
563
|
+
this.setTiddlerInfo({ title: fields.title, revision_id: fields.revision_id, bag_name: bag_name });
|
|
564
|
+
onNext(fields);
|
|
371
565
|
});
|
|
566
|
+
onDone();
|
|
567
|
+
}
|
|
568
|
+
async saveTiddlers(options: {
|
|
569
|
+
syncer: Syncer<MWSAdaptorInfo>;
|
|
570
|
+
tiddlers: Tiddler[];
|
|
571
|
+
onNext: (title: string, adaptorInfo: MWSAdaptorInfo, revision: string) => void;
|
|
572
|
+
onDone: () => void;
|
|
573
|
+
onError: (err: Error) => void;
|
|
574
|
+
}) {
|
|
575
|
+
const { syncer, tiddlers, onNext, onDone, onError } = options;
|
|
372
576
|
|
|
373
|
-
|
|
374
|
-
const { data: tiddlerInfoArray = [] } = result;
|
|
577
|
+
const fields = tiddlers.map(e => e.getFieldStrings());
|
|
375
578
|
|
|
376
|
-
|
|
579
|
+
const results = await this.rpcRequest({
|
|
580
|
+
key: "rpcSaveRecipeTiddlerList",
|
|
581
|
+
body: { tiddlers: fields }
|
|
582
|
+
}).catch(err => {
|
|
583
|
+
onError(err);
|
|
584
|
+
throw new Error("Error saving tiddlers: " + err.message);
|
|
585
|
+
});
|
|
377
586
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
modifications.push(tiddlerInfo.title);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
);
|
|
587
|
+
results.forEach((tiddler) => {
|
|
588
|
+
this.setTiddlerInfo({ title: tiddler.title, revision_id: tiddler.revision_id, bag_name: tiddler.bag.bag_name });
|
|
589
|
+
onNext(tiddler.title, {
|
|
590
|
+
bag: tiddler.bag.bag_name,
|
|
591
|
+
title: tiddler.title,
|
|
592
|
+
revision: tiddler.revision_id
|
|
593
|
+
}, tiddler.revision_id);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
onDone();
|
|
597
|
+
}
|
|
393
598
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
599
|
+
async deleteTiddlers(options: { syncer: Syncer<MWSAdaptorInfo>; titles: string[]; onNext: (title: string) => void; onDone: () => void; onError: (err: Error) => void; }) {
|
|
600
|
+
const { syncer, titles, onNext, onDone, onError } = options;
|
|
601
|
+
|
|
602
|
+
const results = await this.rpcRequest({
|
|
603
|
+
key: "rpcDeleteRecipeTiddlerList",
|
|
604
|
+
body: { titles: titles as PrismaField<"Tiddlers", "title">[] }
|
|
605
|
+
}).catch(err => {
|
|
606
|
+
onError(err);
|
|
607
|
+
throw new Error("Error deleting tiddlers: " + err.message);
|
|
398
608
|
});
|
|
399
609
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
$tw.browserStorage.addCachedTiddlers();
|
|
404
|
-
}
|
|
610
|
+
results.forEach(({ title, revision_id, bag_id }) => {
|
|
611
|
+
this.removeTiddlerInfo({ title, revision_id, bag_id });
|
|
612
|
+
onNext(title);
|
|
405
613
|
});
|
|
614
|
+
|
|
615
|
+
onDone();
|
|
406
616
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
checkLastRecordedUpdate(title: string, revision: string) {
|
|
411
|
-
var lru = this.lastRecordedUpdate[title];
|
|
412
|
-
if (lru) {
|
|
413
|
-
var numRevision = $tw.utils.getInt(revision, 0);
|
|
414
|
-
if (!numRevision) {
|
|
415
|
-
this.logger.log("Error: revision is not a number", revision);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
console.log(`Checking for updates to ${title} since ${JSON.stringify(revision)} comparing to ${numRevision}`);
|
|
419
|
-
if (lru.tiddler_id > numRevision) {
|
|
420
|
-
this.syncer && this.syncer.enqueueLoadTiddler(title);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
get syncer() {
|
|
425
|
-
//@ts-expect-error
|
|
426
|
-
if ($tw.syncadaptor === this) return $tw.syncer;
|
|
427
|
-
}
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
428
620
|
/*
|
|
429
621
|
Save a tiddler and invoke the callback with (err,adaptorInfo,revision)
|
|
430
622
|
*/
|
|
431
623
|
async saveTiddler(
|
|
432
624
|
tiddler: Tiddler,
|
|
433
|
-
callback: (
|
|
625
|
+
callback: (
|
|
626
|
+
err: any,
|
|
627
|
+
adaptorInfo?: MWSAdaptorInfo,
|
|
628
|
+
revision?: string
|
|
629
|
+
) => void,
|
|
434
630
|
options?: {}
|
|
435
631
|
) {
|
|
436
|
-
var self = this, title = tiddler.fields.title;
|
|
632
|
+
var self = this, title = tiddler.fields.title as string;
|
|
633
|
+
if (title === "$:/StoryList") {
|
|
634
|
+
return callback(null);
|
|
635
|
+
}
|
|
437
636
|
if (this.isReadOnly || title.substr(0, MWC_STATE_TIDDLER_PREFIX.length) === MWC_STATE_TIDDLER_PREFIX) {
|
|
438
637
|
return callback(null);
|
|
439
638
|
}
|
|
440
639
|
self.outstandingRequests[title] = { type: "PUT" };
|
|
441
|
-
|
|
442
|
-
|
|
640
|
+
|
|
641
|
+
// application/x-mws-tiddler
|
|
642
|
+
// The .tid file format does not support field names with colons.
|
|
643
|
+
// Rather than trying to catch all the unsupported variations that may appear,
|
|
644
|
+
// we'll just use JSON to send it across the wire, since that is the official fallback format anyway.
|
|
645
|
+
// However, parsing a huge string value inside a JSON object is very slow,
|
|
646
|
+
// so we split off the text field and send it after the other fields.
|
|
647
|
+
|
|
648
|
+
const fields = tiddler.getFieldStrings({});
|
|
649
|
+
const text = fields.text;
|
|
650
|
+
delete fields.text;
|
|
651
|
+
let body = JSON.stringify(fields);
|
|
652
|
+
|
|
443
653
|
if (tiddler.hasField("text")) {
|
|
444
|
-
if (typeof
|
|
445
|
-
return callback(new Error("Error saving tiddler " +
|
|
446
|
-
body += `\n\n${
|
|
654
|
+
if (typeof text !== "string" && text)
|
|
655
|
+
return callback(new Error("Error saving tiddler " + fields.title + ": the text field is truthy but not a string"));
|
|
656
|
+
body += `\n\n${text}`
|
|
447
657
|
}
|
|
448
658
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
659
|
+
type t = RouteTypes["handleSaveRecipeTiddler"]
|
|
660
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
661
|
+
key: "handleSaveRecipeTiddler",
|
|
662
|
+
url: "/tiddlers/" + encodeURIComponent(title),
|
|
663
|
+
method: "PUT",
|
|
664
|
+
requestBodyString: body,
|
|
452
665
|
headers: {
|
|
453
|
-
"
|
|
454
|
-
}
|
|
455
|
-
data: body,
|
|
456
|
-
responseType: "json",
|
|
666
|
+
"content-type": "application/x-mws-tiddler"
|
|
667
|
+
}
|
|
457
668
|
});
|
|
669
|
+
|
|
458
670
|
delete self.outstandingRequests[title];
|
|
459
671
|
if (!ok) return callback(err);
|
|
460
|
-
|
|
672
|
+
|
|
673
|
+
const data = result.responseJSON;
|
|
674
|
+
if (!data) return callback(null); // a 2xx response without a body is unlikely
|
|
461
675
|
|
|
462
676
|
//If Browser-Storage plugin is present, remove tiddler from local storage after successful sync to the server
|
|
463
677
|
if ($tw.browserStorage && $tw.browserStorage.isEnabled()) {
|
|
464
678
|
$tw.browserStorage.removeTiddlerFromLocalStorage(title);
|
|
465
679
|
}
|
|
466
680
|
|
|
681
|
+
|
|
467
682
|
// Save the details of the new revision of the tiddler
|
|
468
|
-
const revision = data.
|
|
683
|
+
const revision = data.revision_id, bag_name = data.bag_name;
|
|
469
684
|
console.log(`Saved ${title} with revision ${revision} and bag ${bag_name}`);
|
|
470
685
|
// If there has been a more recent update from the server then enqueue a load of this tiddler
|
|
471
686
|
self.checkLastRecordedUpdate(title, revision);
|
|
472
687
|
// Invoke the callback
|
|
473
|
-
self.setTiddlerInfo(title, revision, bag_name);
|
|
474
|
-
callback(null, { bag: bag_name }, revision);
|
|
688
|
+
self.setTiddlerInfo({ title, revision_id: revision, bag_name: bag_name });
|
|
689
|
+
callback(null, { bag: bag_name, title: title, revision: revision }, revision);
|
|
475
690
|
|
|
476
691
|
}
|
|
477
692
|
/*
|
|
@@ -480,24 +695,29 @@ class MultiWikiClientAdaptor {
|
|
|
480
695
|
The syncer does not pass itself into options.
|
|
481
696
|
*/
|
|
482
697
|
async loadTiddler(title: string, callback: (err: any, fields?: any) => void, options: any) {
|
|
483
|
-
|
|
698
|
+
const self = this;
|
|
699
|
+
// if (!self.syncer) return callback(new Error("No syncer available"));
|
|
484
700
|
self.outstandingRequests[title] = { type: "GET" };
|
|
485
|
-
const [ok, err, result] = await this.
|
|
486
|
-
|
|
487
|
-
|
|
701
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
702
|
+
key: "handleLoadRecipeTiddler",
|
|
703
|
+
url: "/tiddlers/" + encodeURIComponent(title),
|
|
704
|
+
method: "GET",
|
|
705
|
+
})
|
|
488
706
|
delete self.outstandingRequests[title];
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
707
|
+
|
|
708
|
+
if (!ok) return callback(err);
|
|
709
|
+
|
|
710
|
+
const { responseJSON: data, headers } = result;
|
|
711
|
+
const revision = headers.get("x-revision-number") ?? "",
|
|
712
|
+
bag_name = headers.get("x-bag-name") ?? "";
|
|
713
|
+
|
|
714
|
+
if (!revision || !bag_name || !data) return callback(null, null);
|
|
715
|
+
|
|
496
716
|
// If there has been a more recent update from the server then enqueue a load of this tiddler
|
|
497
717
|
self.checkLastRecordedUpdate(title, revision);
|
|
498
718
|
// Invoke the callback
|
|
499
|
-
self.setTiddlerInfo(title, revision, bag_name);
|
|
500
|
-
callback(null,
|
|
719
|
+
self.setTiddlerInfo({ title, revision_id: revision, bag_name: bag_name });
|
|
720
|
+
callback(null, data);
|
|
501
721
|
}
|
|
502
722
|
/*
|
|
503
723
|
Delete a tiddler and invoke the callback with (err)
|
|
@@ -512,23 +732,254 @@ class MultiWikiClientAdaptor {
|
|
|
512
732
|
// if(!bag) { return callback(null, options.tiddlerInfo.adaptorInfo); }
|
|
513
733
|
self.outstandingRequests[title] = { type: "DELETE" };
|
|
514
734
|
// Issue HTTP request to delete the tiddler
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
735
|
+
type t = RouteTypes["handleDeleteRecipeTiddler"]
|
|
736
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
737
|
+
key: "handleDeleteRecipeTiddler",
|
|
738
|
+
url: "/tiddlers/" + encodeURIComponent(title),
|
|
739
|
+
method: "DELETE",
|
|
518
740
|
});
|
|
519
741
|
delete self.outstandingRequests[title];
|
|
520
|
-
if (!ok)
|
|
521
|
-
const { data } = result;
|
|
522
|
-
|
|
742
|
+
if (!ok) return callback(err);
|
|
743
|
+
const { responseJSON: data } = result;
|
|
744
|
+
if (!data) return callback(null);
|
|
745
|
+
|
|
746
|
+
const { revision_id, bag_name, bag_id } = data;
|
|
523
747
|
// If there has been a more recent update from the server then enqueue a load of this tiddler
|
|
524
|
-
self.checkLastRecordedUpdate(title,
|
|
525
|
-
self.removeTiddlerInfo(title);
|
|
748
|
+
self.checkLastRecordedUpdate(title, revision_id);
|
|
749
|
+
self.removeTiddlerInfo({ title, revision_id, bag_id });
|
|
526
750
|
// Invoke the callback & return null adaptorInfo
|
|
527
751
|
callback(null, null);
|
|
528
752
|
}
|
|
753
|
+
|
|
754
|
+
async rpcRequest<T extends string & keyof RouteTypes>(
|
|
755
|
+
options: {
|
|
756
|
+
key: T;
|
|
757
|
+
body: zod.infer<RouteTypes[T]["T"]>;
|
|
758
|
+
}
|
|
759
|
+
): Promise<RouteTypes[T]["R"]> {
|
|
760
|
+
|
|
761
|
+
type t = RouteTypes["handleSaveRecipeTiddler"]
|
|
762
|
+
const [ok, err, result] = await this.recipeRequest({
|
|
763
|
+
key: options.key,
|
|
764
|
+
url: "/rpc/" + options.key,
|
|
765
|
+
method: "PUT",
|
|
766
|
+
requestBodyString: JSON.stringify(options.body),
|
|
767
|
+
headers: {
|
|
768
|
+
"content-type": "application/json"
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
if (!ok) throw err;
|
|
773
|
+
|
|
774
|
+
if (!result.responseJSON) throw new Error("No response JSON returned");
|
|
775
|
+
|
|
776
|
+
return result.responseJSON as RouteTypes[T]["R"];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* This throws an error if the response status is not 2xx, but will still return the response alongside the error.
|
|
781
|
+
*
|
|
782
|
+
* So if the first parameter is false, the third parameter may still contain the response.
|
|
783
|
+
*/
|
|
784
|
+
private async recipeRequest<KEY extends (string & keyof RouteTypes)>(
|
|
785
|
+
options: Omit<HttpRequestOptions<"arraybuffer">, "responseType"> & { key: KEY }
|
|
786
|
+
) {
|
|
787
|
+
if (!options.url.startsWith("/"))
|
|
788
|
+
throw new Error("The url does not start with a slash");
|
|
789
|
+
|
|
790
|
+
return await httpRequest({
|
|
791
|
+
...options,
|
|
792
|
+
responseType: "blob",
|
|
793
|
+
url: this.host + "recipe/" + encodeURIComponent(this.recipe) + options.url,
|
|
794
|
+
}).then(async e => {
|
|
795
|
+
if (!e.ok) return [
|
|
796
|
+
false, new Error(
|
|
797
|
+
`The server return a status code ${e.status} with the following reason: `
|
|
798
|
+
+ `${e.headers.get("x-reason") ?? "(no reason given)"}`
|
|
799
|
+
), {
|
|
800
|
+
...e,
|
|
801
|
+
responseString: "",
|
|
802
|
+
responseJSON: undefined,
|
|
803
|
+
}
|
|
804
|
+
] as const;
|
|
805
|
+
let responseString: string = "";
|
|
806
|
+
if (e.headers.get("x-gzip-stream") === "yes") {
|
|
807
|
+
// Browsers only decode the first stream,
|
|
808
|
+
// so we cant use content-encoding or DecompressionStream
|
|
809
|
+
|
|
810
|
+
await new Promise<void>(async resolve => {
|
|
811
|
+
|
|
812
|
+
const gunzip = new fflate.AsyncGunzip((err, chunk, final) => {
|
|
813
|
+
if (err) return console.log(err);
|
|
814
|
+
responseString += fflate.strFromU8(chunk);
|
|
815
|
+
if (final) resolve();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (this.isDevMode) gunzip.onmember = e => console.log("gunzip member", e);
|
|
819
|
+
|
|
820
|
+
gunzip.push(new Uint8Array(await readBlobAsArrayBuffer(e.response)));
|
|
821
|
+
// this has to be on a separate line
|
|
822
|
+
gunzip.push(new Uint8Array(), true);
|
|
823
|
+
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
if (this.isDevMode) console.log("gunzip result", responseString.length);
|
|
827
|
+
|
|
828
|
+
} else {
|
|
829
|
+
responseString = fflate.strFromU8(new Uint8Array(await readBlobAsArrayBuffer(e.response)));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return [true, void 0, {
|
|
833
|
+
...e,
|
|
834
|
+
responseString,
|
|
835
|
+
/** this is undefined if status is not 200 */
|
|
836
|
+
responseJSON: e.status === 200
|
|
837
|
+
? tryParseJSON(responseString) as RouteTypes[KEY]["R"]
|
|
838
|
+
: undefined,
|
|
839
|
+
}] as const;
|
|
840
|
+
}, e => [false, e, void 0] as const);
|
|
841
|
+
|
|
842
|
+
function tryParseJSON(data: string) {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(data);
|
|
845
|
+
} catch (e) {
|
|
846
|
+
console.error("Error parsing JSON, returning undefined", e);
|
|
847
|
+
// console.log(data);
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
}
|
|
853
|
+
|
|
529
854
|
}
|
|
530
855
|
|
|
531
856
|
|
|
532
857
|
if ($tw.browser && document.location.protocol.startsWith("http")) {
|
|
533
858
|
exports.adaptorClass = MultiWikiClientAdaptor;
|
|
534
859
|
}
|
|
860
|
+
|
|
861
|
+
type ParamsInput = URLSearchParams | [string, string][] | object | string | undefined;
|
|
862
|
+
|
|
863
|
+
interface HttpRequestOptions<TYPE extends "arraybuffer" | "blob" | "text"> {
|
|
864
|
+
/** The request METHOD. Maybe be anything except CONNECT, TRACE, or TRACK. */
|
|
865
|
+
method: string;
|
|
866
|
+
/** The url may also contain query params. */
|
|
867
|
+
url: string;
|
|
868
|
+
/** The response types */
|
|
869
|
+
responseType: TYPE;
|
|
870
|
+
headers?: HeadersInit;
|
|
871
|
+
/** This is parsed separately from the url and appended to it. */
|
|
872
|
+
queryParams?: ParamsInput;
|
|
873
|
+
/**
|
|
874
|
+
* The string to send as the request body. Not valid for GET and HEAD.
|
|
875
|
+
*
|
|
876
|
+
* For `application/x-www-form-urlencoded`, use `new URLSearchParams().toString()`.
|
|
877
|
+
*
|
|
878
|
+
* For `application/json`, use `JSON.stringify()`
|
|
879
|
+
*/
|
|
880
|
+
requestBodyString?: string;
|
|
881
|
+
progress?: (event: ProgressEvent<EventTarget>) => void;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
function httpRequest<TYPE extends "arraybuffer" | "blob" | "text">(options: HttpRequestOptions<TYPE>) {
|
|
886
|
+
|
|
887
|
+
return new Promise<{
|
|
888
|
+
/** Shorthand to check if the response is in the 2xx range. */
|
|
889
|
+
ok: boolean;
|
|
890
|
+
status: number;
|
|
891
|
+
statusText: string;
|
|
892
|
+
headers: Headers;
|
|
893
|
+
response:
|
|
894
|
+
TYPE extends "arraybuffer" ? ArrayBuffer :
|
|
895
|
+
TYPE extends "blob" ? Blob :
|
|
896
|
+
TYPE extends "text" ? string :
|
|
897
|
+
never;
|
|
898
|
+
}>((resolve, reject) => {
|
|
899
|
+
// if this throws sync'ly, the promise will reject.
|
|
900
|
+
|
|
901
|
+
options.method = options.method.toUpperCase();
|
|
902
|
+
|
|
903
|
+
if ((options.method === "GET" || options.method === "HEAD") && options.requestBodyString)
|
|
904
|
+
throw new Error("requestBodyString must be falsy if method is GET or HEAD");
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
const url = new URL(options.url, location.href);
|
|
908
|
+
const query = paramsInput(options.queryParams);
|
|
909
|
+
query.forEach((v, k) => { url.searchParams.append(k, v); });
|
|
910
|
+
|
|
911
|
+
const headers = new Headers(options.headers || {});
|
|
912
|
+
|
|
913
|
+
const request = new XMLHttpRequest();
|
|
914
|
+
request.responseType = options.responseType || "text";
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
request.open(options.method, url, true);
|
|
918
|
+
} catch (e) { console.log(e, { e }); throw e; }
|
|
919
|
+
|
|
920
|
+
if (!headers.has("content-type"))
|
|
921
|
+
headers.set("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
|
922
|
+
|
|
923
|
+
if (!headers.has("x-requested-with"))
|
|
924
|
+
headers.set("x-requested-with", "TiddlyWiki");
|
|
925
|
+
|
|
926
|
+
headers.set("accept", "application/json");
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
headers.forEach((v, k) => {
|
|
930
|
+
request.setRequestHeader(k, v);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
request.onreadystatechange = function () {
|
|
934
|
+
if (this.readyState !== 4) return;
|
|
935
|
+
|
|
936
|
+
const headers = new Headers();
|
|
937
|
+
request.getAllResponseHeaders()?.trim().split(/[\r\n]+/).forEach((line) => {
|
|
938
|
+
const parts = line.split(": ");
|
|
939
|
+
const header = parts.shift()?.toLowerCase();
|
|
940
|
+
const value = parts.join(": ");
|
|
941
|
+
if (header) headers.append(header, value);
|
|
942
|
+
});
|
|
943
|
+
resolve({
|
|
944
|
+
ok: this.status >= 200 && this.status < 300,
|
|
945
|
+
status: this.status,
|
|
946
|
+
statusText: this.statusText,
|
|
947
|
+
response: this.response,
|
|
948
|
+
headers,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
if (options.progress)
|
|
954
|
+
request.onprogress = options.progress;
|
|
955
|
+
|
|
956
|
+
try { request.send(options.requestBodyString); } catch (e) { console.log(e, { e }); throw e; }
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
function paramsInput(input: ParamsInput) {
|
|
963
|
+
if (!input) return new URLSearchParams();
|
|
964
|
+
if (input instanceof URLSearchParams) return input;
|
|
965
|
+
if (Array.isArray(input) || typeof input === "string") return new URLSearchParams(input);
|
|
966
|
+
return new URLSearchParams(Object.entries(input));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function readBlobAsArrayBuffer(blob: Blob) {
|
|
972
|
+
const error = new Error("Error reading blob");
|
|
973
|
+
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
974
|
+
const reader = new FileReader();
|
|
975
|
+
reader.onload = () => {
|
|
976
|
+
resolve(reader.result as ArrayBuffer)
|
|
977
|
+
};
|
|
978
|
+
reader.onerror = () => {
|
|
979
|
+
reject(error);
|
|
980
|
+
};
|
|
981
|
+
reader.readAsArrayBuffer(blob);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
}
|
|
985
|
+
|