@memizy/host-sdk 1.0.0-rc.6 → 1.0.0-rc.7
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/learning.cjs +1 -1
- package/dist/learning.js +3 -1
- package/dist/plugin.cjs +1 -1
- package/dist/plugin.js +12 -12
- package/dist/session.cjs +1 -1
- package/dist/session.js +39 -13
- package/dist/src/learning.d.ts +2 -0
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/session.d.ts +17 -3
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog of @memizy/host-sdk
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.7 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
* `LocalSession` gives the authority a secret random seed (`InitPayload.seed`; `SessionConfig.seed` only for replays and tests).
|
|
6
|
+
* The default seed of the display order (`shuffleSeed`) is random, no longer the session id: a player knows the session id and could undo the shuffle (authors often write sets in the answer's order). Pass the same `shuffleSeed` to continue a saved game.
|
|
7
|
+
* `getAsset` gives a player's instance in multiplayer only media of its own (public) set: media of the back of a flashcard or of an explanation can no longer be fetched by key.
|
|
8
|
+
* `recordAnswer` is accepted only from the authority (SPEC 5): another instance cannot prove an answer, so a modified player's device cannot write "correct" into a teacher's statistics.
|
|
9
|
+
* Answers to `open-ended` items are kept, not graded: the `answer` event has `ungraded: true` and no record, the learning progress is unchanged. Every `answer` event carries `setId` (an item is identified by set + item ID).
|
|
10
|
+
* `prepareSetForPlugin` leaves out items whose `mustUnderstand` names properties this version does not support.
|
|
11
|
+
* `leitner` keeps the learner's `starred` and fields of a later OQSEP version (also in `stats`) and stores the answer as `lastAnswer.answer`; the `LearningAlgorithm` contract says so.
|
|
12
|
+
* `LocalSession` gives settings of a later protocol version their default and ignores values sent for them.
|
|
13
|
+
* Public items of a session come from the new fail-closed `publicItem` of `@memizy/protocol`.
|
|
14
|
+
|
|
3
15
|
## 1.0.0-rc.6 (2026-10-14)
|
|
4
16
|
|
|
5
17
|
Enforces the final protocol 1.0 ([SPEC](https://github.com/memizy/memizy/blob/main/packages/protocol/SPEC.md)).
|
package/README.md
CHANGED
|
@@ -61,6 +61,6 @@ const player = new RelayPlayer({ serverUrl: 'https://mp.memizy.com', pin, name:
|
|
|
61
61
|
|
|
62
62
|
`uploadBundle` takes the `PreparedSet` and uploads only its `publicSet`: anyone with the PIN can download the bundle, so answers never leave the host. Use the same seed for the bundle and `shuffleSeed`, so every device shows the same order.
|
|
63
63
|
|
|
64
|
-
**Continuing a saved game** (SPEC 3.3): create the session with the saved players (their old IDs), the same `sessionId`, `shuffleSeed` and storage, `restore: true` and a paused `clock`, then `host.attach(session, { seats: true })`. Devices see the earlier places in `state.seats` and take one with `player.claimSeat(id)` (or `claimSeat(null)` to join as a new player when the game allows late joins); the host can change it with `host.assignSeat(roomPlayerId, seatId)` (`host.seats` lists the places with their `roomPlayerId`). Keep `host.seatAssignments` and pass it as `taken` to attach again after a reload of the host page. Memizy Play (
|
|
64
|
+
**Continuing a saved game** (SPEC 3.3): create the session with the saved players (their old IDs), the same `sessionId`, `shuffleSeed` and storage, `restore: true` and a paused `clock`, then `host.attach(session, { seats: true })`. Devices see the earlier places in `state.seats` and take one with `player.claimSeat(id)` (or `claimSeat(null)` to join as a new player when the game allows late joins); the host can change it with `host.assignSeat(roomPlayerId, seatId)` (`host.seats` lists the places with their `roomPlayerId`). Keep `host.seatAssignments` and pass it as `taken` to attach again after a reload of the host page. Memizy Play ([memizy.com/play](https://memizy.com/play/)) uses exactly this flow.
|
|
65
65
|
|
|
66
66
|
Iframes are sandboxed with `allow-scripts allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox` (`PLUGIN_SANDBOX`; links open in a new tab) and **never** `allow-same-origin` or `allow-top-navigation`.
|
package/dist/learning.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=[0,1,3,7,21],t={id:`leitner`,apply(t,n,r){let i=t?.stats??{attempts:0,incorrect:0,streak:0},a={isCorrect:!n.isSkipped&&n.isCorrect,answeredAt:r.toISOString(),...n.confidence!==void 0&&{confidence:n.confidence},...n.timeSpentMs!==void 0&&{timeSpent:n.timeSpentMs},...n.hintsUsed!==void 0&&{hintsUsed:n.hintsUsed},...n.isSkipped&&{isSkipped:!0}};if(n.isSkipped)return{...t??{bucket:0},stats:i,lastAnswer:a};let o=n.isCorrect?Math.min(4,Math.max(1,t?.bucket??0)+1):1,s=new Date(r.getTime()+e[o]*24*60*60*1e3);return{...t,bucket:o,nextReviewAt:s.toISOString(),stats:{attempts:i.attempts+1,incorrect:i.incorrect+ +!n.isCorrect,streak:n.isCorrect?i.streak+1:0},lastAnswer:a}}};exports.LEITNER_INTERVAL_DAYS=e,exports.leitner=t;
|
|
1
|
+
var e=[0,1,3,7,21],t={id:`leitner`,apply(t,n,r){let i=t?.stats??{attempts:0,incorrect:0,streak:0},a={isCorrect:!n.isSkipped&&n.isCorrect,answeredAt:r.toISOString(),...n.confidence!==void 0&&{confidence:n.confidence},...n.timeSpentMs!==void 0&&{timeSpent:n.timeSpentMs},...n.hintsUsed!==void 0&&{hintsUsed:n.hintsUsed},...n.isSkipped&&{isSkipped:!0},...n.answer!==void 0&&{answer:n.answer}};if(n.isSkipped)return{...t??{bucket:0},stats:i,lastAnswer:a};let o=n.isCorrect?Math.min(4,Math.max(1,t?.bucket??0)+1):1,s=new Date(r.getTime()+e[o]*24*60*60*1e3);return{...t,bucket:o,nextReviewAt:s.toISOString(),stats:{...i,attempts:i.attempts+1,incorrect:i.incorrect+ +!n.isCorrect,streak:n.isCorrect?i.streak+1:0},lastAnswer:a}}};exports.LEITNER_INTERVAL_DAYS=e,exports.leitner=t;
|
package/dist/learning.js
CHANGED
|
@@ -18,7 +18,8 @@ var e = [
|
|
|
18
18
|
...n.confidence !== void 0 && { confidence: n.confidence },
|
|
19
19
|
...n.timeSpentMs !== void 0 && { timeSpent: n.timeSpentMs },
|
|
20
20
|
...n.hintsUsed !== void 0 && { hintsUsed: n.hintsUsed },
|
|
21
|
-
...n.isSkipped && { isSkipped: !0 }
|
|
21
|
+
...n.isSkipped && { isSkipped: !0 },
|
|
22
|
+
...n.answer !== void 0 && { answer: n.answer }
|
|
22
23
|
};
|
|
23
24
|
if (n.isSkipped) return {
|
|
24
25
|
...t ?? { bucket: 0 },
|
|
@@ -31,6 +32,7 @@ var e = [
|
|
|
31
32
|
bucket: o,
|
|
32
33
|
nextReviewAt: s.toISOString(),
|
|
33
34
|
stats: {
|
|
35
|
+
...i,
|
|
34
36
|
attempts: i.attempts + 1,
|
|
35
37
|
incorrect: i.incorrect + +!n.isCorrect,
|
|
36
38
|
streak: n.isCorrect ? i.streak + 1 : 0
|
package/dist/plugin.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let e=require("@memizy/protocol"),t=require("@memizy/oqse");function n(t){let n=(0,e.readPluginManifestFromHtml)(t);return n.success?(0,e.isProtocolSupported)(n.runtime.protocol)?{success:!0,plugin:{html:t,manifest:n.manifest,runtime:n.runtime}}:{success:!1,errors:[`The plugin needs Memizy Plugin Protocol ${n.runtime.protocol}, which this host does not support.`]}:n}async function r(e,t=fetch){let r=await t(e);return r.ok?n(await r.text()):{success:!1,errors:[`Could not download the plugin (${r.status} ${r.statusText}).`]}}function i(n,r,i={}){let a=r.capabilities?.types??[],o=a.includes(`*`)?n.items:n.items.filter(e=>a.includes(e.type)),s=(0,e.prepareDisplaySet)({meta:n.meta,items:o},i.seed);return{set:s.set,publicSet:s.publicSet,compatibility:(0,t.checkCompatibility)(n,r),skippedItems:n.items.length-o.length}}function a(e){let n=new Date().toISOString();return{version:t.OQSE_VERSION,meta:{id:(0,t.generateUUID)(),title:e.title,language:e.language,createdAt:n,updatedAt:n},items:[]}}exports.createEmptySet=a,exports.loadPluginFromHtml=n,exports.loadPluginFromUrl=r,exports.prepareSetForPlugin=i;
|
|
1
|
+
let e=require("@memizy/protocol"),t=require("@memizy/oqse");function n(t){let n=(0,e.readPluginManifestFromHtml)(t);return n.success?(0,e.isProtocolSupported)(n.runtime.protocol)?{success:!0,plugin:{html:t,manifest:n.manifest,runtime:n.runtime}}:{success:!1,errors:[`The plugin needs Memizy Plugin Protocol ${n.runtime.protocol}, which this host does not support.`]}:n}async function r(e,t=fetch){let r=await t(e);return r.ok?n(await r.text()):{success:!1,errors:[`Could not download the plugin (${r.status} ${r.statusText}).`]}}function i(n,r,i={}){let a=r.capabilities?.types??[],o=(a.includes(`*`)?n.items:n.items.filter(e=>a.includes(e.type))).filter(e=>(0,t.missingMustUnderstand)(e).length===0),s=(0,e.prepareDisplaySet)({meta:n.meta,items:o},i.seed);return{set:s.set,publicSet:s.publicSet,compatibility:(0,t.checkCompatibility)(n,r),skippedItems:n.items.length-o.length}}function a(e){let n=new Date().toISOString();return{version:t.OQSE_VERSION,meta:{id:(0,t.generateUUID)(),title:e.title,language:e.language,createdAt:n,updatedAt:n},items:[]}}exports.createEmptySet=a,exports.loadPluginFromHtml=n,exports.loadPluginFromUrl=r,exports.prepareSetForPlugin=i;
|
package/dist/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isProtocolSupported as e, prepareDisplaySet as t, readPluginManifestFromHtml as n } from "@memizy/protocol";
|
|
2
|
-
import { OQSE_VERSION as r, checkCompatibility as i, generateUUID as a } from "@memizy/oqse";
|
|
2
|
+
import { OQSE_VERSION as r, checkCompatibility as i, generateUUID as a, missingMustUnderstand as o } from "@memizy/oqse";
|
|
3
3
|
//#region src/plugin.ts
|
|
4
|
-
function
|
|
4
|
+
function s(t) {
|
|
5
5
|
let r = n(t);
|
|
6
6
|
return r.success ? e(r.runtime.protocol) ? {
|
|
7
7
|
success: !0,
|
|
@@ -15,26 +15,26 @@ function o(t) {
|
|
|
15
15
|
errors: [`The plugin needs Memizy Plugin Protocol ${r.runtime.protocol}, which this host does not support.`]
|
|
16
16
|
} : r;
|
|
17
17
|
}
|
|
18
|
-
async function
|
|
18
|
+
async function c(e, t = fetch) {
|
|
19
19
|
let n = await t(e);
|
|
20
|
-
return n.ok ?
|
|
20
|
+
return n.ok ? s(await n.text()) : {
|
|
21
21
|
success: !1,
|
|
22
22
|
errors: [`Could not download the plugin (${n.status} ${n.statusText}).`]
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
function
|
|
26
|
-
let a = n.capabilities?.types ?? [],
|
|
25
|
+
function l(e, n, r = {}) {
|
|
26
|
+
let a = n.capabilities?.types ?? [], s = (a.includes("*") ? e.items : e.items.filter((e) => a.includes(e.type))).filter((e) => o(e).length === 0), c = t({
|
|
27
27
|
meta: e.meta,
|
|
28
|
-
items:
|
|
28
|
+
items: s
|
|
29
29
|
}, r.seed);
|
|
30
30
|
return {
|
|
31
|
-
set:
|
|
32
|
-
publicSet:
|
|
31
|
+
set: c.set,
|
|
32
|
+
publicSet: c.publicSet,
|
|
33
33
|
compatibility: i(e, n),
|
|
34
|
-
skippedItems: e.items.length -
|
|
34
|
+
skippedItems: e.items.length - s.length
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
function
|
|
37
|
+
function u(e) {
|
|
38
38
|
let t = (/* @__PURE__ */ new Date()).toISOString();
|
|
39
39
|
return {
|
|
40
40
|
version: r,
|
|
@@ -49,4 +49,4 @@ function l(e) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
52
|
-
export {
|
|
52
|
+
export { u as createEmptySet, s as loadPluginFromHtml, c as loadPluginFromUrl, l as prepareSetForPlugin };
|
package/dist/session.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
require("./version.cjs");const e=require("./plugin.cjs"),t=require("./learning.cjs"),n=require("./storage.cjs");let r=require("@memizy/protocol"),i=require("@memizy/oqse"),a=require("@memizy/protocol/internal");function o(e){try{return structuredClone(e)}catch{return JSON.parse(JSON.stringify(e))}}var s=class{id;plugin;runtime;mode;hostAs;authority;prepared;roster;settingValues;settingsOk=!0;hostConfig;isStarted=!1;isEnded=!1;pauseState={pausedMs:0,pausedAt:null};options;storage;algorithm;instances=new Map;listeners=new Set;authorityConnected=!0;startPromise=null;resumeStartPending=!1;pendingSnapshot=void 0;finished=!1;hasResult=!1;finishedSnapshot=null;snapshotTimer;pendingData=new Map;get players(){return this.roster}get settings(){return this.settingValues}get settingsValid(){return this.settingsOk}get config(){return this.hostConfig}get started(){return this.isStarted}get ended(){return this.isEnded}constructor(i){if(this.options=i,this.id=i.sessionId??`session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`,this.plugin=i.plugin,this.runtime=i.plugin.runtime,this.mode=i.mode,this.storage=i.storage??new n.MemoryStorage,this.algorithm=i.algorithm??t.leitner,this.hostConfig={locale:i.locale??`en`,theme:i.theme??`light`},i.mode===`solo`){if(!this.runtime.solo)throw Error(`The plugin does not support solo mode.`);this.hostAs=null}else{if(!this.runtime.multiplayer)throw Error(`The plugin does not support multiplayer.`);let e=i.hostAs??this.runtime.multiplayer.hostAs[0];if(!this.runtime.multiplayer.hostAs.includes(e))throw Error(`The plugin does not support hostAs "${e}".`);this.hostAs=e}let a=i.players??(i.mode===`solo`?[{id:`player`,name:`Player`,isHost:!0}]:[]);if(i.mode===`solo`&&a.length!==1)throw Error(`A solo session has exactly one player.`);if(new Set(a.map(e=>e.id)).size!==a.length)throw Error(`Player IDs must be unique.`);if(a.some(e=>e.id===r.BOARD_ADDRESS||e.id===r.SETTINGS_ADDRESS||e.id===`server`))throw Error(`Player IDs "board", "settings" and "server" are reserved.`);if(this.roster=a.map(e=>({id:e.id,name:e.name,isHost:i.mode===`solo`?!0:e.isHost??!1,connected:!0})),i.mode===`solo`)this.authority=this.roster[0].id;else if(this.hostAs===`presenter`)this.authority=r.BOARD_ADDRESS;else{let e=this.roster.find(e=>e.isHost);if(!e)throw Error(`With hostAs "player", one player must have isHost: true.`);this.authority=e.id}let s=(0,r.resolveSettings)(this.runtime.settings,i.settings??{});if(s.errors.length>0)throw Error(`Invalid settings: ${s.errors.join(`; `)}`);this.settingValues=s.values;let c=i.shuffleSeed===null?void 0:i.shuffleSeed??this.id;this.prepared=e.prepareSetForPlugin(o(i.set),i.plugin.manifest,{seed:c}),i.restore&&(this.isStarted=!0,this.resumeStartPending=!0),i.clock&&(this.pauseState={pausedMs:i.clock.pausedMs??0,pausedAt:i.clock.pausedAt??null})}on(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)try{t(e)}catch(e){console.error(`[memizy host] session listener failed:`,e)}}viewOf(e){return e===r.SETTINGS_ADDRESS?`settings`:this.mode===`solo`?`solo`:e===r.BOARD_ADDRESS?`board`:`controller`}addresses(){return[...this.hostAs===`presenter`?[r.BOARD_ADDRESS]:[],...this.roster.map(e=>e.id)]}async connect(e,t,n={}){let i=this.viewOf(e);if(i===`settings`){if(!this.runtime.multiplayer||!this.runtime.settingsScreen)throw Error(`The plugin has no settings screen (settingsScreen).`);if(this.isStarted)throw Error(`The settings screen is only available before the game starts.`)}else if(!this.addresses().includes(e))throw Error(`Unknown address "${e}" in this session.`);if(this.isEnded)throw Error(`The session has ended.`);this.disposeInstance(e);let o={address:e,view:i,hostApi:void 0,endpoint:t,onDispose:n.onDispose,plugin:null,ready:!1,connected:!0,disposed:!1,resumed:!1,greeted:!1,limiter:new a.RateLimiter(r.LIMITS.messagesPerSecond,r.LIMITS.messageBurst),serviceLimiter:new a.RateLimiter(r.LIMITS.servicesPerMinute/60,10),answerLimiter:new a.RateLimiter(r.LIMITS.answersPerSecond,r.LIMITS.answerBurst),assetLimiter:new a.RateLimiter(r.LIMITS.assetsPerSecond,r.LIMITS.assetBurst),errorLimiter:new a.RateLimiter(r.LIMITS.errorReportsPerSecond,r.LIMITS.errorReportBurst)};o.hostApi=this.createHostApi(o),this.instances.set(e,o),e===this.authority&&this.isStarted&&this.setAuthorityConnected(!1);try{o.plugin=await t(o.hostApi)}catch(e){if(o.disposed)return o;throw e}return o}async reload(e){let t=this.instances.get(e);if(!t)throw Error(`No instance at "${e}".`);return e===this.authority&&await this.flushSnapshot(),this.connect(e,t.endpoint,{onDispose:t.onDispose})}disconnect(e){this.disposeInstance(e),e===this.authority&&this.isStarted&&this.setAuthorityConnected(!1)}disposeInstance(e){let t=this.instances.get(e);if(t){t.disposed=!0,this.instances.delete(e);try{t.onDispose?.()}catch(e){console.warn(`[memizy host] dispose failed:`,e)}}}getInstance(e){return this.instances.get(e)}start(){if(this.mode===`solo`||this.isStarted&&!this.startPromise)return Promise.resolve();if(!this.settingsOk)return Promise.reject(Error(`The settings are not valid.`));let e=this.roster.length,t=this.runtime.multiplayer.players;return e<t.min||e>t.max?Promise.reject(Error(`This plugin needs ${t.min}–${t.max} players (now ${e}).`)):(this.startPromise??=this.runStart(),this.startPromise)}async runStart(){this.disposeInstance(r.SETTINGS_ADDRESS);let e=Date.now()+(this.options.readyTimeoutMs??15e3),t=()=>[...this.instances.values()].every(e=>e.ready),n=()=>this.instances.get(this.authority)?.ready===!0;for(;!(t()&&n())&&(Date.now()<e||!n());){if(this.isEnded)return;await c(25)}let i=this.options.countdownMs??3e3;for(let e=Math.ceil(i/1e3);e>0;e--)this.emit({type:`countdown`,secondsLeft:e}),await c(Math.min(1e3,i-(Math.ceil(i/1e3)-e)*1e3));this.isEnded||(this.isStarted=!0,this.emit({type:`started`}),await this.callPlugin(this.authority,e=>e.start()))}addPlayer(e){if(this.mode===`solo`)throw Error(`Players cannot join a solo session.`);if(this.roster.some(t=>t.id===e.id)||[r.BOARD_ADDRESS,r.SETTINGS_ADDRESS,`server`].includes(e.id))throw Error(`Player ID "${e.id}" is already used or reserved.`);if(this.isStarted&&!this.runtime.multiplayer.lateJoin)throw Error(`This game does not allow joining after the start.`);if(this.roster.length>=this.runtime.multiplayer.players.max)throw Error(`The game is full.`);this.roster=[...this.roster,{id:e.id,name:e.name,isHost:!1,connected:!0}],this.broadcastPlayers()}removePlayer(e){if(e===this.authority)throw Error(`The host cannot be removed.`);this.disposeInstance(e),this.roster=this.roster.filter(t=>t.id!==e),this.broadcastPlayers()}renamePlayer(e,t){this.roster=this.roster.map(n=>n.id===e?{...n,name:t}:n),this.broadcastPlayers()}setConnected(e,t){let n=this.instances.get(e);n&&(n.connected=t),this.roster.some(t=>t.id===e)&&(this.roster=this.roster.map(n=>n.id===e?{...n,connected:t}:n),this.broadcastPlayers()),e===this.authority?this.setAuthorityConnected(t):t&&n&&this.callPlugin(e,e=>e.authorityChanged({connected:!0}))}get services(){return this.runtime.services.filter(e=>typeof this.options.services?.[e]==`function`)}get paused(){return this.pauseState.pausedAt!==null}get clock(){return{offsetMs:0,...this.pauseState}}pause(){!this.isStarted||this.isEnded||this.paused||(this.pauseState={...this.pauseState,pausedAt:Date.now()},this.broadcastClock())}resume(){this.paused&&(this.pauseState={pausedMs:this.pauseState.pausedMs+Math.max(0,Date.now()-this.pauseState.pausedAt),pausedAt:null},this.broadcastClock())}broadcastClock(){let e=this.clock;for(let t of this.instances.keys())this.callPlugin(t,t=>t.clockChanged(e));this.emit({type:`paused`,paused:this.paused,clock:{...this.pauseState}})}setConfig(e){this.hostConfig={...this.hostConfig,...e};for(let e of this.instances.keys())this.callPlugin(e,e=>e.configChanged(this.hostConfig))}async end(e=`closed`){this.isEnded||(this.isEnded=!0,e===`finished`?await this.discardSnapshot():await this.flushSnapshot(),await this.flushData(),await Promise.all([...this.instances.keys()].map(t=>this.callPlugin(t,t=>t.sessionEnded(e)))))}createHostApi(e){let t=(t,n)=>(async()=>{if(e.disposed)throw new r.ProtocolError(`SESSION_ENDED`,`This instance has been closed.`);if(this.isEnded&&t!==`reportError`)throw new r.ProtocolError(`SESSION_ENDED`,`The session has ended.`);return n()})().catch(n=>{let i=(0,r.toProtocolError)(n);throw this.emit({type:`rejected`,address:e.address,method:t,code:i.code,message:i.detail}),i}),n=()=>e.view===`solo`||e.view===`controller`,s=(t,n)=>{if(!n)throw new r.ProtocolError(`NOT_ALLOWED_IN_VIEW`,`${t} is not available in view "${e.view}".`)},c=t=>{if(e.address!==this.authority)throw new r.ProtocolError(`NOT_AUTHORITY`,`${t} may only be called by the authority.`)};return{hello:(...n)=>t(`hello`,async()=>{let[t]=(0,a.parseHostCall)(`hello`,n);if(e.greeted)throw new r.ProtocolError(`INVALID_ARGUMENT`,`hello may only be called once per instance.`);let i=(0,r.negotiateProtocolVersion)(t.protocol,r.PROTOCOL_VERSION);if(!i)throw new r.ProtocolError(`UNSUPPORTED_PROTOCOL`,`The plugin speaks protocol ${t.protocol}; this host supports ${r.PROTOCOL_VERSION}.`);return e.greeted=!0,t.plugin.id!==this.plugin.manifest.id&&this.emit({type:`pluginError`,address:e.address,code:`PLUGIN_ID_MISMATCH`,message:`The SDK reports plugin id "${t.plugin.id}", the manifest says "${this.plugin.manifest.id}".`}),this.buildInit(e,i)}),ready:(...n)=>t(`ready`,()=>{(0,a.parseHostCall)(`ready`,n),!e.ready&&(e.ready=!0,this.emit({type:`ready`,address:e.address}),e.address===this.authority&&(this.mode===`solo`&&!this.isStarted&&!e.resumed?(this.isStarted=!0,this.emit({type:`started`}),this.callPlugin(e.address,e=>e.start())):this.isStarted&&(this.resumeStartPending&&(this.resumeStartPending=!1,e.resumed||this.callPlugin(e.address,e=>e.start())),this.setAuthorityConnected(!0))))}),send:(...n)=>t(`send`,()=>{let[t]=(0,a.parseHostCall)(`send`,n);s(`send`,e.view!==`settings`);let i=(0,a.assertJsonWithin)(t.data,r.LIMITS.messageBytes,`MESSAGE_TOO_LARGE`,`message`),o=e.address===this.authority?1/Math.max(1,this.instances.size-1):1;if(!e.limiter.tryTake(o))throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.messagesPerSecond} messages per second.`);if(!e.connected)return;if(t.to===`authority`){let n=this.instances.get(this.authority);if(!n||!n.connected||!this.authorityConnected)throw new r.ProtocolError(`AUTHORITY_UNAVAILABLE`,`The host is not reachable right now.`);this.deliver(e.address,this.authority,t.data,i);return}let c=t.to===`all`?[...this.instances.keys()].filter(t=>t!==e.address&&t!==r.SETTINGS_ADDRESS):t.to.filter(e=>e!==r.SETTINGS_ADDRESS);for(let n of c)this.deliver(e.address,n,t.data,i)}),saveSnapshot:(...e)=>t(`saveSnapshot`,()=>{let[t]=(0,a.parseHostCall)(`saveSnapshot`,e);if(c(`saveSnapshot`),(0,a.assertJsonWithin)(t,r.LIMITS.snapshotBytes,`SNAPSHOT_TOO_LARGE`,`snapshot`),this.finished){this.finishedSnapshot=structuredClone(t);return}this.pendingSnapshot=structuredClone(t),this.snapshotTimer??=setTimeout(()=>void this.flushSnapshot(),1e3/r.LIMITS.snapshotsPerSecond)}),recordAnswer:(...n)=>t(`recordAnswer`,async()=>{let[t]=(0,a.parseHostCall)(`recordAnswer`,n),i=this.roster.some(t=>t.id===e.address)?e.address:null,o=t.playerId??i;if(!o)throw new r.ProtocolError(`INVALID_ARGUMENT`,`recordAnswer needs a playerId on the board.`);if(o!==i&&c(`recordAnswer for another player`),!this.roster.some(e=>e.id===o))throw new r.ProtocolError(`INVALID_ARGUMENT`,`Unknown player "${o}".`);let s=e.address===this.authority?1/Math.max(1,this.roster.length):1;if(!e.answerLimiter.tryTake(s))throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.answersPerSecond} answers per second.`);t.answer!==void 0&&(0,a.assertJsonWithin)(t.answer,r.LIMITS.answerBytes,`DATA_TOO_LARGE`,`answer`);let l=this.prepared.set.items.find(e=>e.id===t.itemId),u=e=>Array.isArray(e)?e.filter(e=>typeof e==`string`):[];if(!l){if(!t.item)throw new r.ProtocolError(`INVALID_ARGUMENT`,`Unknown item "${t.itemId}" (attach the item for a generated one).`);(0,a.assertJsonWithin)(t.item,r.LIMITS.generatedItemBytes,`DATA_TOO_LARGE`,`item`),this.emit({type:`answer`,playerId:o,record:null,answer:t,generated:!0,skills:u(t.item.skills),tags:u(t.item.tags)});return}let d=this.userKey(o),f=this.prepared.set.meta.id,p=await this.storage.loadProgress(d,f),{item:m,...h}=t,g=this.algorithm.apply(p[t.itemId],h,new Date,structuredClone(l));await this.storage.saveProgress(d,f,{[t.itemId]:g}),this.emit({type:`answer`,playerId:o,record:g,answer:h,generated:!1,skills:u(l.skills),tags:u(l.tags)})}),saveData:(...i)=>t(`saveData`,()=>{let[t,o]=(0,a.parseHostCall)(`saveData`,i);s(`saveData`,n()),(0,a.assertJsonWithin)(o,r.LIMITS.dataBytes,`DATA_TOO_LARGE`,`plugin data (${t})`);let c=`${e.address}|${t}`,l=this.pendingData.get(c);if(l)l.value=structuredClone(o);else{let n={playerId:e.address,scope:t,value:structuredClone(o),timer:setTimeout(()=>void this.flushData(c),1e3/r.LIMITS.dataWritesPerSecond)};this.pendingData.set(c,n)}}),updateSettings:(...n)=>t(`updateSettings`,()=>{let[t]=(0,a.parseHostCall)(`updateSettings`,n);s(`updateSettings`,e.view===`settings`);let{values:i,errors:o}=(0,r.resolveSettings)(this.runtime.settings,t.values);this.settingValues=i,this.settingsOk=t.valid&&o.length===0;let c=t.message??o[0];this.emit({type:`settings`,values:i,valid:this.settingsOk,...c&&{message:c}})}),getAsset:(...n)=>t(`getAsset`,async()=>{let[t,o]=(0,a.parseHostCall)(`getAsset`,n);if(!e.assetLimiter.tryTake())throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.assetsPerSecond} assets per second.`);let s=o?this.options.set.items.find(e=>e.id===o):void 0,c=(0,i.resolveAsset)(t,s,this.options.set.meta);if(!c)throw new r.ProtocolError(`ASSET_NOT_FOUND`,`Asset "${t}" does not exist.`);if(this.options.loadAsset)return this.options.loadAsset(t,o);if(/^https?:/i.test(c.value)){let e=await fetch(c.value);if(e.ok)return e.blob()}throw new r.ProtocolError(`ASSET_NOT_FOUND`,`Asset "${t}" cannot be loaded by this host.`)}),end:(...e)=>t(`end`,async()=>{let[t]=(0,a.parseHostCall)(`end`,e);c(`end`),!this.hasResult&&(this.hasResult=!0,await this.discardSnapshot(),this.emit({type:`result`,result:t}))}),reportError:(...n)=>t(`reportError`,()=>{let[t]=(0,a.parseHostCall)(`reportError`,n);e.errorLimiter.tryTake()&&this.emit({type:`pluginError`,address:e.address,code:t.code,message:t.message,...t.context&&{context:t.context}})}),service:(...n)=>t(`service`,async()=>{let[t,i]=(0,a.parseHostCall)(`service`,n),s=this.services.includes(t)?this.options.services[t]:void 0;if(!s)throw new r.ProtocolError(`SERVICE_UNAVAILABLE`,`Service "${t}" is not available here (declare it in the manifest "services"; this host offers: ${this.services.join(`, `)||`none`}).`);if((0,a.assertJsonWithin)(i,r.LIMITS.serviceBytes,`MESSAGE_TOO_LARGE`,`service ${t} request`),!e.serviceLimiter.tryTake())throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.servicesPerMinute} service calls per minute.`);let c=this.roster.some(t=>t.id===e.address)?e.address:null,l=await s(o(i),{playerId:c,address:e.address,pluginId:this.plugin.manifest.id,setId:this.prepared.set.meta.id});return(0,a.assertJsonWithin)(l??null,r.LIMITS.serviceResultBytes,`MESSAGE_TOO_LARGE`,`service ${t} result`),o(l??null)})}}async buildInit(e,t){let n=e.view===`solo`||e.view===`controller`,r=this.prepared.set.meta.id,i=n?this.userKey(e.address):null,a=this.plugin.manifest.id;e.address===this.authority&&await this.flushSnapshot();let o=e.address===this.authority?this.finished?structuredClone(this.finishedSnapshot):await this.storage.loadSnapshot(this.id):null;return e.resumed=o!==null,{protocol:t,host:this.options.host??{name:`@memizy/host-sdk`,version:`1.0.0-rc.6`},oqseVersion:`1.0`,features:[],session:{id:this.id,mode:this.mode,hostAs:this.hostAs,view:e.view,self:e.address,authority:this.authority,lateJoin:this.isStarted&&e.view!==`settings`},players:this.roster,set:structuredClone(this.mode===`multiplayer`&&e.address!==this.authority?this.prepared.publicSet:this.prepared.set),settings:{...this.settingValues},config:this.hostConfig,clock:this.clock,services:this.services,progress:i?await this.storage.loadProgress(i,r):{},data:i?{plugin:this.pendingDataValue(e.address,`plugin`)??await this.storage.loadData(i,a,`plugin`,r)??null,set:this.pendingDataValue(e.address,`set`)??await this.storage.loadData(i,a,`set`,r)??null}:{plugin:null,set:null},snapshot:o}}userKey(e){return this.options.userKeyFor?.(e)??e}deliver(e,t,n,r){let i=this.instances.get(t);if(!i||!i.connected||!i.plugin)return;this.emit({type:`traffic`,from:e,to:t,bytes:r,data:n});let a=structuredClone(n);setTimeout(()=>void this.callPlugin(t,t=>t.deliver({from:e,data:a,sentAt:Date.now()})),0)}broadcastPlayers(){this.emit({type:`players`,players:this.roster});for(let e of this.instances.keys())this.callPlugin(e,e=>e.playersChanged(this.roster))}setAuthorityConnected(e){if(this.authorityConnected!==e){this.authorityConnected=e,this.emit({type:`authority`,connected:e});for(let t of this.instances.keys())t!==this.authority&&t!==r.SETTINGS_ADDRESS&&this.callPlugin(t,t=>t.authorityChanged({connected:e}))}}async callPlugin(e,t){let n=this.instances.get(e);if(n?.plugin&&!n.disposed)try{await t(n.plugin)}catch(t){this.emit({type:`pluginError`,address:e,code:`PLUGIN_CALL_FAILED`,message:(0,r.toProtocolError)(t).detail})}}async discardSnapshot(){this.finished||(this.finished=!0,clearTimeout(this.snapshotTimer),this.snapshotTimer=void 0,this.finishedSnapshot=this.pendingSnapshot===void 0?await this.storage.loadSnapshot(this.id):this.pendingSnapshot,this.pendingSnapshot=void 0,await this.storage.deleteSnapshot?.(this.id))}async flushSnapshot(){if(clearTimeout(this.snapshotTimer),this.snapshotTimer=void 0,this.pendingSnapshot===void 0||this.finished)return;let e=this.pendingSnapshot;this.pendingSnapshot=void 0,await this.storage.saveSnapshot(this.id,e),this.finished&&await this.storage.deleteSnapshot?.(this.id)}pendingDataValue(e,t){return this.pendingData.get(`${e}|${t}`)?.value}async flushData(e){let t=e?[[e,this.pendingData.get(e)]]:[...this.pendingData.entries()];for(let[e,n]of t)n&&(clearTimeout(n.timer),this.pendingData.delete(e),await this.storage.saveData(this.userKey(n.playerId),this.plugin.manifest.id,n.scope,this.prepared.set.meta.id,n.value))}};function c(e){return new Promise(t=>setTimeout(t,Math.max(0,e)))}exports.LocalSession=s;
|
|
1
|
+
require("./version.cjs");const e=require("./plugin.cjs"),t=require("./learning.cjs"),n=require("./storage.cjs");let r=require("@memizy/protocol"),i=require("@memizy/oqse"),a=require("@memizy/protocol/internal");function o(e){try{return structuredClone(e)}catch{return JSON.parse(JSON.stringify(e))}}var s=class{id;seed;plugin;runtime;mode;hostAs;authority;prepared;roster;settingValues;settingsOk=!0;hostConfig;isStarted=!1;isEnded=!1;pauseState={pausedMs:0,pausedAt:null};options;storage;algorithm;instances=new Map;listeners=new Set;authorityConnected=!0;startPromise=null;resumeStartPending=!1;pendingSnapshot=void 0;finished=!1;hasResult=!1;finishedSnapshot=null;snapshotTimer;pendingData=new Map;get players(){return this.roster}get settings(){return this.settingValues}get settingsValid(){return this.settingsOk}get config(){return this.hostConfig}get started(){return this.isStarted}get ended(){return this.isEnded}constructor(i){if(this.options=i,this.id=i.sessionId??`session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`,this.seed=i.seed??l(),this.plugin=i.plugin,this.runtime=i.plugin.runtime,this.mode=i.mode,this.storage=i.storage??new n.MemoryStorage,this.algorithm=i.algorithm??t.leitner,this.hostConfig={locale:i.locale??`en`,theme:i.theme??`light`},i.mode===`solo`){if(!this.runtime.solo)throw Error(`The plugin does not support solo mode.`);this.hostAs=null}else{if(!this.runtime.multiplayer)throw Error(`The plugin does not support multiplayer.`);let e=i.hostAs??this.runtime.multiplayer.hostAs[0];if(!this.runtime.multiplayer.hostAs.includes(e))throw Error(`The plugin does not support hostAs "${e}".`);this.hostAs=e}let a=i.players??(i.mode===`solo`?[{id:`player`,name:`Player`,isHost:!0}]:[]);if(i.mode===`solo`&&a.length!==1)throw Error(`A solo session has exactly one player.`);if(new Set(a.map(e=>e.id)).size!==a.length)throw Error(`Player IDs must be unique.`);if(a.some(e=>e.id===r.BOARD_ADDRESS||e.id===r.SETTINGS_ADDRESS||e.id===`server`))throw Error(`Player IDs "board", "settings" and "server" are reserved.`);if(this.roster=a.map(e=>({id:e.id,name:e.name,isHost:i.mode===`solo`?!0:e.isHost??!1,connected:!0})),i.mode===`solo`)this.authority=this.roster[0].id;else if(this.hostAs===`presenter`)this.authority=r.BOARD_ADDRESS;else{let e=this.roster.find(e=>e.isHost);if(!e)throw Error(`With hostAs "player", one player must have isHost: true.`);this.authority=e.id}let s=(0,r.resolveSettings)(this.runtime.settings,i.settings??{},this.runtime.unknownSettings);if(s.errors.length>0)throw Error(`Invalid settings: ${s.errors.join(`; `)}`);this.settingValues=s.values;let c=i.shuffleSeed===null?void 0:i.shuffleSeed??l();this.prepared=e.prepareSetForPlugin(o(i.set),i.plugin.manifest,{seed:c}),i.restore&&(this.isStarted=!0,this.resumeStartPending=!0),i.clock&&(this.pauseState={pausedMs:i.clock.pausedMs??0,pausedAt:i.clock.pausedAt??null})}on(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)try{t(e)}catch(e){console.error(`[memizy host] session listener failed:`,e)}}viewOf(e){return e===r.SETTINGS_ADDRESS?`settings`:this.mode===`solo`?`solo`:e===r.BOARD_ADDRESS?`board`:`controller`}addresses(){return[...this.hostAs===`presenter`?[r.BOARD_ADDRESS]:[],...this.roster.map(e=>e.id)]}async connect(e,t,n={}){let i=this.viewOf(e);if(i===`settings`){if(!this.runtime.multiplayer||!this.runtime.settingsScreen)throw Error(`The plugin has no settings screen (settingsScreen).`);if(this.isStarted)throw Error(`The settings screen is only available before the game starts.`)}else if(!this.addresses().includes(e))throw Error(`Unknown address "${e}" in this session.`);if(this.isEnded)throw Error(`The session has ended.`);this.disposeInstance(e);let o={address:e,view:i,hostApi:void 0,endpoint:t,onDispose:n.onDispose,plugin:null,ready:!1,connected:!0,disposed:!1,resumed:!1,greeted:!1,limiter:new a.RateLimiter(r.LIMITS.messagesPerSecond,r.LIMITS.messageBurst),serviceLimiter:new a.RateLimiter(r.LIMITS.servicesPerMinute/60,10),answerLimiter:new a.RateLimiter(r.LIMITS.answersPerSecond,r.LIMITS.answerBurst),assetLimiter:new a.RateLimiter(r.LIMITS.assetsPerSecond,r.LIMITS.assetBurst),errorLimiter:new a.RateLimiter(r.LIMITS.errorReportsPerSecond,r.LIMITS.errorReportBurst)};o.hostApi=this.createHostApi(o),this.instances.set(e,o),e===this.authority&&this.isStarted&&this.setAuthorityConnected(!1);try{o.plugin=await t(o.hostApi)}catch(e){if(o.disposed)return o;throw e}return o}async reload(e){let t=this.instances.get(e);if(!t)throw Error(`No instance at "${e}".`);return e===this.authority&&await this.flushSnapshot(),this.connect(e,t.endpoint,{onDispose:t.onDispose})}disconnect(e){this.disposeInstance(e),e===this.authority&&this.isStarted&&this.setAuthorityConnected(!1)}disposeInstance(e){let t=this.instances.get(e);if(t){t.disposed=!0,this.instances.delete(e);try{t.onDispose?.()}catch(e){console.warn(`[memizy host] dispose failed:`,e)}}}getInstance(e){return this.instances.get(e)}start(){if(this.mode===`solo`||this.isStarted&&!this.startPromise)return Promise.resolve();if(!this.settingsOk)return Promise.reject(Error(`The settings are not valid.`));let e=this.roster.length,t=this.runtime.multiplayer.players;return e<t.min||e>t.max?Promise.reject(Error(`This plugin needs ${t.min}–${t.max} players (now ${e}).`)):(this.startPromise??=this.runStart(),this.startPromise)}async runStart(){this.disposeInstance(r.SETTINGS_ADDRESS);let e=Date.now()+(this.options.readyTimeoutMs??15e3),t=()=>[...this.instances.values()].every(e=>e.ready),n=()=>this.instances.get(this.authority)?.ready===!0;for(;!(t()&&n())&&(Date.now()<e||!n());){if(this.isEnded)return;await c(25)}let i=this.options.countdownMs??3e3;for(let e=Math.ceil(i/1e3);e>0;e--)this.emit({type:`countdown`,secondsLeft:e}),await c(Math.min(1e3,i-(Math.ceil(i/1e3)-e)*1e3));this.isEnded||(this.isStarted=!0,this.emit({type:`started`}),await this.callPlugin(this.authority,e=>e.start()))}addPlayer(e){if(this.mode===`solo`)throw Error(`Players cannot join a solo session.`);if(this.roster.some(t=>t.id===e.id)||[r.BOARD_ADDRESS,r.SETTINGS_ADDRESS,`server`].includes(e.id))throw Error(`Player ID "${e.id}" is already used or reserved.`);if(this.isStarted&&!this.runtime.multiplayer.lateJoin)throw Error(`This game does not allow joining after the start.`);if(this.roster.length>=this.runtime.multiplayer.players.max)throw Error(`The game is full.`);this.roster=[...this.roster,{id:e.id,name:e.name,isHost:!1,connected:!0}],this.broadcastPlayers()}removePlayer(e){if(e===this.authority)throw Error(`The host cannot be removed.`);this.disposeInstance(e),this.roster=this.roster.filter(t=>t.id!==e),this.broadcastPlayers()}renamePlayer(e,t){this.roster=this.roster.map(n=>n.id===e?{...n,name:t}:n),this.broadcastPlayers()}setConnected(e,t){let n=this.instances.get(e);n&&(n.connected=t),this.roster.some(t=>t.id===e)&&(this.roster=this.roster.map(n=>n.id===e?{...n,connected:t}:n),this.broadcastPlayers()),e===this.authority?this.setAuthorityConnected(t):t&&n&&this.callPlugin(e,e=>e.authorityChanged({connected:!0}))}get services(){return this.runtime.services.filter(e=>typeof this.options.services?.[e]==`function`)}get paused(){return this.pauseState.pausedAt!==null}get clock(){return{offsetMs:0,...this.pauseState}}pause(){!this.isStarted||this.isEnded||this.paused||(this.pauseState={...this.pauseState,pausedAt:Date.now()},this.broadcastClock())}resume(){this.paused&&(this.pauseState={pausedMs:this.pauseState.pausedMs+Math.max(0,Date.now()-this.pauseState.pausedAt),pausedAt:null},this.broadcastClock())}broadcastClock(){let e=this.clock;for(let t of this.instances.keys())this.callPlugin(t,t=>t.clockChanged(e));this.emit({type:`paused`,paused:this.paused,clock:{...this.pauseState}})}setConfig(e){this.hostConfig={...this.hostConfig,...e};for(let e of this.instances.keys())this.callPlugin(e,e=>e.configChanged(this.hostConfig))}async end(e=`closed`){this.isEnded||(this.isEnded=!0,e===`finished`?await this.discardSnapshot():await this.flushSnapshot(),await this.flushData(),await Promise.all([...this.instances.keys()].map(t=>this.callPlugin(t,t=>t.sessionEnded(e)))))}createHostApi(e){let t=(t,n)=>(async()=>{if(e.disposed)throw new r.ProtocolError(`SESSION_ENDED`,`This instance has been closed.`);if(this.isEnded&&t!==`reportError`)throw new r.ProtocolError(`SESSION_ENDED`,`The session has ended.`);return n()})().catch(n=>{let i=(0,r.toProtocolError)(n);throw this.emit({type:`rejected`,address:e.address,method:t,code:i.code,message:i.detail}),i}),n=()=>e.view===`solo`||e.view===`controller`,s=(t,n)=>{if(!n)throw new r.ProtocolError(`NOT_ALLOWED_IN_VIEW`,`${t} is not available in view "${e.view}".`)},c=t=>{if(e.address!==this.authority)throw new r.ProtocolError(`NOT_AUTHORITY`,`${t} may only be called by the authority.`)};return{hello:(...n)=>t(`hello`,async()=>{let[t]=(0,a.parseHostCall)(`hello`,n);if(e.greeted)throw new r.ProtocolError(`INVALID_ARGUMENT`,`hello may only be called once per instance.`);let i=(0,r.negotiateProtocolVersion)(t.protocol,r.PROTOCOL_VERSION);if(!i)throw new r.ProtocolError(`UNSUPPORTED_PROTOCOL`,`The plugin speaks protocol ${t.protocol}; this host supports ${r.PROTOCOL_VERSION}.`);return e.greeted=!0,t.plugin.id!==this.plugin.manifest.id&&this.emit({type:`pluginError`,address:e.address,code:`PLUGIN_ID_MISMATCH`,message:`The SDK reports plugin id "${t.plugin.id}", the manifest says "${this.plugin.manifest.id}".`}),this.buildInit(e,i)}),ready:(...n)=>t(`ready`,()=>{(0,a.parseHostCall)(`ready`,n),!e.ready&&(e.ready=!0,this.emit({type:`ready`,address:e.address}),e.address===this.authority&&(this.mode===`solo`&&!this.isStarted&&!e.resumed?(this.isStarted=!0,this.emit({type:`started`}),this.callPlugin(e.address,e=>e.start())):this.isStarted&&(this.resumeStartPending&&(this.resumeStartPending=!1,e.resumed||this.callPlugin(e.address,e=>e.start())),this.setAuthorityConnected(!0))))}),send:(...n)=>t(`send`,()=>{let[t]=(0,a.parseHostCall)(`send`,n);s(`send`,e.view!==`settings`);let i=(0,a.assertJsonWithin)(t.data,r.LIMITS.messageBytes,`MESSAGE_TOO_LARGE`,`message`),o=e.address===this.authority?1/Math.max(1,this.instances.size-1):1;if(!e.limiter.tryTake(o))throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.messagesPerSecond} messages per second.`);if(!e.connected)return;if(t.to===`authority`){let n=this.instances.get(this.authority);if(!n||!n.connected||!this.authorityConnected)throw new r.ProtocolError(`AUTHORITY_UNAVAILABLE`,`The host is not reachable right now.`);this.deliver(e.address,this.authority,t.data,i);return}let c=t.to===`all`?[...this.instances.keys()].filter(t=>t!==e.address&&t!==r.SETTINGS_ADDRESS):t.to.filter(e=>e!==r.SETTINGS_ADDRESS);for(let n of c)this.deliver(e.address,n,t.data,i)}),saveSnapshot:(...e)=>t(`saveSnapshot`,()=>{let[t]=(0,a.parseHostCall)(`saveSnapshot`,e);if(c(`saveSnapshot`),(0,a.assertJsonWithin)(t,r.LIMITS.snapshotBytes,`SNAPSHOT_TOO_LARGE`,`snapshot`),this.finished){this.finishedSnapshot=structuredClone(t);return}this.pendingSnapshot=structuredClone(t),this.snapshotTimer??=setTimeout(()=>void this.flushSnapshot(),1e3/r.LIMITS.snapshotsPerSecond)}),recordAnswer:(...n)=>t(`recordAnswer`,async()=>{let[t]=(0,a.parseHostCall)(`recordAnswer`,n),i=this.roster.some(t=>t.id===e.address)?e.address:null,o=t.playerId??i;if(!o)throw new r.ProtocolError(`INVALID_ARGUMENT`,`recordAnswer needs a playerId on the board.`);if(c(`recordAnswer`),!this.roster.some(e=>e.id===o))throw new r.ProtocolError(`INVALID_ARGUMENT`,`Unknown player "${o}".`);let s=e.address===this.authority?1/Math.max(1,this.roster.length):1;if(!e.answerLimiter.tryTake(s))throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.answersPerSecond} answers per second.`);t.answer!==void 0&&(0,a.assertJsonWithin)(t.answer,r.LIMITS.answerBytes,`DATA_TOO_LARGE`,`answer`);let l=this.prepared.set.items.find(e=>e.id===t.itemId),u=e=>Array.isArray(e)?e.filter(e=>typeof e==`string`):[];if(!l){if(!t.item)throw new r.ProtocolError(`INVALID_ARGUMENT`,`Unknown item "${t.itemId}" (attach the item for a generated one).`);(0,a.assertJsonWithin)(t.item,r.LIMITS.generatedItemBytes,`DATA_TOO_LARGE`,`item`),this.emit({type:`answer`,playerId:o,setId:this.prepared.set.meta.id,record:null,answer:t,generated:!0,ungraded:!1,skills:u(t.item.skills),tags:u(t.item.tags)});return}let d=this.prepared.set.meta.id;if(l.type===`open-ended`){let{item:e,...n}=t;this.emit({type:`answer`,playerId:o,setId:d,record:null,answer:n,generated:!1,ungraded:!0,skills:u(l.skills),tags:u(l.tags)});return}let f=this.userKey(o),p=await this.storage.loadProgress(f,d),{item:m,...h}=t,g=this.algorithm.apply(p[t.itemId],h,new Date,structuredClone(l));await this.storage.saveProgress(f,d,{[t.itemId]:g}),this.emit({type:`answer`,playerId:o,setId:d,record:g,answer:h,generated:!1,ungraded:!1,skills:u(l.skills),tags:u(l.tags)})}),saveData:(...i)=>t(`saveData`,()=>{let[t,o]=(0,a.parseHostCall)(`saveData`,i);s(`saveData`,n()),(0,a.assertJsonWithin)(o,r.LIMITS.dataBytes,`DATA_TOO_LARGE`,`plugin data (${t})`);let c=`${e.address}|${t}`,l=this.pendingData.get(c);if(l)l.value=structuredClone(o);else{let n={playerId:e.address,scope:t,value:structuredClone(o),timer:setTimeout(()=>void this.flushData(c),1e3/r.LIMITS.dataWritesPerSecond)};this.pendingData.set(c,n)}}),updateSettings:(...n)=>t(`updateSettings`,()=>{let[t]=(0,a.parseHostCall)(`updateSettings`,n);s(`updateSettings`,e.view===`settings`);let{values:i,errors:o}=(0,r.resolveSettings)(this.runtime.settings,t.values,this.runtime.unknownSettings);this.settingValues=i,this.settingsOk=t.valid&&o.length===0;let c=t.message??o[0];this.emit({type:`settings`,values:i,valid:this.settingsOk,...c&&{message:c}})}),getAsset:(...n)=>t(`getAsset`,async()=>{let[t,o]=(0,a.parseHostCall)(`getAsset`,n);if(!e.assetLimiter.tryTake())throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.assetsPerSecond} assets per second.`);let s=this.mode===`multiplayer`&&e.address!==this.authority?this.prepared.publicSet:this.prepared.set,c=o?s.items.find(e=>e.id===o):void 0,l=(0,i.resolveAsset)(t,c,s.meta);if(!l)throw new r.ProtocolError(`ASSET_NOT_FOUND`,`Asset "${t}" does not exist.`);if(this.options.loadAsset)return this.options.loadAsset(t,o);if(/^https?:/i.test(l.value)){let e=await fetch(l.value);if(e.ok)return e.blob()}throw new r.ProtocolError(`ASSET_NOT_FOUND`,`Asset "${t}" cannot be loaded by this host.`)}),end:(...e)=>t(`end`,async()=>{let[t]=(0,a.parseHostCall)(`end`,e);c(`end`),!this.hasResult&&(this.hasResult=!0,await this.discardSnapshot(),this.emit({type:`result`,result:t}))}),reportError:(...n)=>t(`reportError`,()=>{let[t]=(0,a.parseHostCall)(`reportError`,n);e.errorLimiter.tryTake()&&this.emit({type:`pluginError`,address:e.address,code:t.code,message:t.message,...t.context&&{context:t.context}})}),service:(...n)=>t(`service`,async()=>{let[t,i]=(0,a.parseHostCall)(`service`,n),s=this.services.includes(t)?this.options.services[t]:void 0;if(!s)throw new r.ProtocolError(`SERVICE_UNAVAILABLE`,`Service "${t}" is not available here (declare it in the manifest "services"; this host offers: ${this.services.join(`, `)||`none`}).`);if((0,a.assertJsonWithin)(i,r.LIMITS.serviceBytes,`MESSAGE_TOO_LARGE`,`service ${t} request`),!e.serviceLimiter.tryTake())throw new r.ProtocolError(`RATE_LIMITED`,`More than ${r.LIMITS.servicesPerMinute} service calls per minute.`);let c=this.roster.some(t=>t.id===e.address)?e.address:null,l=await s(o(i),{playerId:c,address:e.address,pluginId:this.plugin.manifest.id,setId:this.prepared.set.meta.id});return(0,a.assertJsonWithin)(l??null,r.LIMITS.serviceResultBytes,`MESSAGE_TOO_LARGE`,`service ${t} result`),o(l??null)})}}async buildInit(e,t){let n=e.view===`solo`||e.view===`controller`,r=this.prepared.set.meta.id,i=n?this.userKey(e.address):null,a=this.plugin.manifest.id;e.address===this.authority&&await this.flushSnapshot();let o=e.address===this.authority?this.finished?structuredClone(this.finishedSnapshot):await this.storage.loadSnapshot(this.id):null;return e.resumed=o!==null,{protocol:t,host:this.options.host??{name:`@memizy/host-sdk`,version:`1.0.0-rc.7`},oqseVersion:`1.0`,features:[],session:{id:this.id,mode:this.mode,hostAs:this.hostAs,view:e.view,self:e.address,authority:this.authority,lateJoin:this.isStarted&&e.view!==`settings`},players:this.roster,set:structuredClone(this.mode===`multiplayer`&&e.address!==this.authority?this.prepared.publicSet:this.prepared.set),settings:{...this.settingValues},config:this.hostConfig,clock:this.clock,services:this.services,progress:i?await this.storage.loadProgress(i,r):{},data:i?{plugin:this.pendingDataValue(e.address,`plugin`)??await this.storage.loadData(i,a,`plugin`,r)??null,set:this.pendingDataValue(e.address,`set`)??await this.storage.loadData(i,a,`set`,r)??null}:{plugin:null,set:null},snapshot:o,...e.address===this.authority&&{seed:this.seed}}}userKey(e){return this.options.userKeyFor?.(e)??e}deliver(e,t,n,r){let i=this.instances.get(t);if(!i||!i.connected||!i.plugin)return;this.emit({type:`traffic`,from:e,to:t,bytes:r,data:n});let a=structuredClone(n);setTimeout(()=>void this.callPlugin(t,t=>t.deliver({from:e,data:a,sentAt:Date.now()})),0)}broadcastPlayers(){this.emit({type:`players`,players:this.roster});for(let e of this.instances.keys())this.callPlugin(e,e=>e.playersChanged(this.roster))}setAuthorityConnected(e){if(this.authorityConnected!==e){this.authorityConnected=e,this.emit({type:`authority`,connected:e});for(let t of this.instances.keys())t!==this.authority&&t!==r.SETTINGS_ADDRESS&&this.callPlugin(t,t=>t.authorityChanged({connected:e}))}}async callPlugin(e,t){let n=this.instances.get(e);if(n?.plugin&&!n.disposed)try{await t(n.plugin)}catch(t){this.emit({type:`pluginError`,address:e,code:`PLUGIN_CALL_FAILED`,message:(0,r.toProtocolError)(t).detail})}}async discardSnapshot(){this.finished||(this.finished=!0,clearTimeout(this.snapshotTimer),this.snapshotTimer=void 0,this.finishedSnapshot=this.pendingSnapshot===void 0?await this.storage.loadSnapshot(this.id):this.pendingSnapshot,this.pendingSnapshot=void 0,await this.storage.deleteSnapshot?.(this.id))}async flushSnapshot(){if(clearTimeout(this.snapshotTimer),this.snapshotTimer=void 0,this.pendingSnapshot===void 0||this.finished)return;let e=this.pendingSnapshot;this.pendingSnapshot=void 0,await this.storage.saveSnapshot(this.id,e),this.finished&&await this.storage.deleteSnapshot?.(this.id)}pendingDataValue(e,t){return this.pendingData.get(`${e}|${t}`)?.value}async flushData(e){let t=e?[[e,this.pendingData.get(e)]]:[...this.pendingData.entries()];for(let[e,n]of t)n&&(clearTimeout(n.timer),this.pendingData.delete(e),await this.storage.saveData(this.userKey(n.playerId),this.plugin.manifest.id,n.scope,this.prepared.set.meta.id,n.value))}};function c(e){return new Promise(t=>setTimeout(t,Math.max(0,e)))}function l(){let e=new Uint8Array(16);return globalThis.crypto.getRandomValues(e),[...e].map(e=>e.toString(16).padStart(2,`0`)).join(``)}exports.LocalSession=s;
|
package/dist/session.js
CHANGED
|
@@ -15,6 +15,7 @@ function h(e) {
|
|
|
15
15
|
}
|
|
16
16
|
var g = class {
|
|
17
17
|
id;
|
|
18
|
+
seed;
|
|
18
19
|
plugin;
|
|
19
20
|
runtime;
|
|
20
21
|
mode;
|
|
@@ -64,7 +65,7 @@ var g = class {
|
|
|
64
65
|
return this.isEnded;
|
|
65
66
|
}
|
|
66
67
|
constructor(i) {
|
|
67
|
-
if (this.options = i, this.id = i.sessionId ?? `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, this.plugin = i.plugin, this.runtime = i.plugin.runtime, this.mode = i.mode, this.storage = i.storage ?? new n(), this.algorithm = i.algorithm ?? t, this.hostConfig = {
|
|
68
|
+
if (this.options = i, this.id = i.sessionId ?? `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, this.seed = i.seed ?? v(), this.plugin = i.plugin, this.runtime = i.plugin.runtime, this.mode = i.mode, this.storage = i.storage ?? new n(), this.algorithm = i.algorithm ?? t, this.hostConfig = {
|
|
68
69
|
locale: i.locale ?? "en",
|
|
69
70
|
theme: i.theme ?? "light"
|
|
70
71
|
}, i.mode === "solo") {
|
|
@@ -96,10 +97,10 @@ var g = class {
|
|
|
96
97
|
if (!e) throw Error("With hostAs \"player\", one player must have isHost: true.");
|
|
97
98
|
this.authority = e.id;
|
|
98
99
|
}
|
|
99
|
-
let o = l(this.runtime.settings, i.settings ?? {});
|
|
100
|
+
let o = l(this.runtime.settings, i.settings ?? {}, this.runtime.unknownSettings);
|
|
100
101
|
if (o.errors.length > 0) throw Error(`Invalid settings: ${o.errors.join("; ")}`);
|
|
101
102
|
this.settingValues = o.values;
|
|
102
|
-
let c = i.shuffleSeed === null ? void 0 : i.shuffleSeed ??
|
|
103
|
+
let c = i.shuffleSeed === null ? void 0 : i.shuffleSeed ?? v();
|
|
103
104
|
this.prepared = e(h(i.set), i.plugin.manifest, { seed: c }), i.restore && (this.isStarted = !0, this.resumeStartPending = !0), i.clock && (this.pauseState = {
|
|
104
105
|
pausedMs: i.clock.pausedMs ?? 0,
|
|
105
106
|
pausedAt: i.clock.pausedAt ?? null
|
|
@@ -338,7 +339,7 @@ var g = class {
|
|
|
338
339
|
recordAnswer: (...n) => t("recordAnswer", async () => {
|
|
339
340
|
let [t] = m("recordAnswer", n), r = this.roster.some((t) => t.id === e.address) ? e.address : null, a = t.playerId ?? r;
|
|
340
341
|
if (!a) throw new o("INVALID_ARGUMENT", "recordAnswer needs a playerId on the board.");
|
|
341
|
-
if (
|
|
342
|
+
if (f("recordAnswer"), !this.roster.some((e) => e.id === a)) throw new o("INVALID_ARGUMENT", `Unknown player "${a}".`);
|
|
342
343
|
let s = e.address === this.authority ? 1 / Math.max(1, this.roster.length) : 1;
|
|
343
344
|
if (!e.answerLimiter.tryTake(s)) throw new o("RATE_LIMITED", `More than ${i.answersPerSecond} answers per second.`);
|
|
344
345
|
t.answer !== void 0 && p(t.answer, i.answerBytes, "DATA_TOO_LARGE", "answer");
|
|
@@ -348,21 +349,41 @@ var g = class {
|
|
|
348
349
|
p(t.item, i.generatedItemBytes, "DATA_TOO_LARGE", "item"), this.emit({
|
|
349
350
|
type: "answer",
|
|
350
351
|
playerId: a,
|
|
352
|
+
setId: this.prepared.set.meta.id,
|
|
351
353
|
record: null,
|
|
352
354
|
answer: t,
|
|
353
355
|
generated: !0,
|
|
356
|
+
ungraded: !1,
|
|
354
357
|
skills: l(t.item.skills),
|
|
355
358
|
tags: l(t.item.tags)
|
|
356
359
|
});
|
|
357
360
|
return;
|
|
358
361
|
}
|
|
359
|
-
let u = this.
|
|
360
|
-
|
|
362
|
+
let u = this.prepared.set.meta.id;
|
|
363
|
+
if (c.type === "open-ended") {
|
|
364
|
+
let { item: e, ...n } = t;
|
|
365
|
+
this.emit({
|
|
366
|
+
type: "answer",
|
|
367
|
+
playerId: a,
|
|
368
|
+
setId: u,
|
|
369
|
+
record: null,
|
|
370
|
+
answer: n,
|
|
371
|
+
generated: !1,
|
|
372
|
+
ungraded: !0,
|
|
373
|
+
skills: l(c.skills),
|
|
374
|
+
tags: l(c.tags)
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
let d = this.userKey(a), h = await this.storage.loadProgress(d, u), { item: g, ..._ } = t, v = this.algorithm.apply(h[t.itemId], _, /* @__PURE__ */ new Date(), structuredClone(c));
|
|
379
|
+
await this.storage.saveProgress(d, u, { [t.itemId]: v }), this.emit({
|
|
361
380
|
type: "answer",
|
|
362
381
|
playerId: a,
|
|
382
|
+
setId: u,
|
|
363
383
|
record: v,
|
|
364
384
|
answer: _,
|
|
365
385
|
generated: !1,
|
|
386
|
+
ungraded: !1,
|
|
366
387
|
skills: l(c.skills),
|
|
367
388
|
tags: l(c.tags)
|
|
368
389
|
});
|
|
@@ -385,7 +406,7 @@ var g = class {
|
|
|
385
406
|
updateSettings: (...n) => t("updateSettings", () => {
|
|
386
407
|
let [t] = m("updateSettings", n);
|
|
387
408
|
r("updateSettings", e.view === "settings");
|
|
388
|
-
let { values: i, errors: a } = l(this.runtime.settings, t.values);
|
|
409
|
+
let { values: i, errors: a } = l(this.runtime.settings, t.values, this.runtime.unknownSettings);
|
|
389
410
|
this.settingValues = i, this.settingsOk = t.valid && a.length === 0;
|
|
390
411
|
let o = t.message ?? a[0];
|
|
391
412
|
this.emit({
|
|
@@ -398,11 +419,11 @@ var g = class {
|
|
|
398
419
|
getAsset: (...n) => t("getAsset", async () => {
|
|
399
420
|
let [t, r] = m("getAsset", n);
|
|
400
421
|
if (!e.assetLimiter.tryTake()) throw new o("RATE_LIMITED", `More than ${i.assetsPerSecond} assets per second.`);
|
|
401
|
-
let a =
|
|
402
|
-
if (!
|
|
422
|
+
let a = this.mode === "multiplayer" && e.address !== this.authority ? this.prepared.publicSet : this.prepared.set, s = r ? a.items.find((e) => e.id === r) : void 0, c = d(t, s, a.meta);
|
|
423
|
+
if (!c) throw new o("ASSET_NOT_FOUND", `Asset "${t}" does not exist.`);
|
|
403
424
|
if (this.options.loadAsset) return this.options.loadAsset(t, r);
|
|
404
|
-
if (/^https?:/i.test(
|
|
405
|
-
let e = await fetch(
|
|
425
|
+
if (/^https?:/i.test(c.value)) {
|
|
426
|
+
let e = await fetch(c.value);
|
|
406
427
|
if (e.ok) return e.blob();
|
|
407
428
|
}
|
|
408
429
|
throw new o("ASSET_NOT_FOUND", `Asset "${t}" cannot be loaded by this host.`);
|
|
@@ -446,7 +467,7 @@ var g = class {
|
|
|
446
467
|
protocol: t,
|
|
447
468
|
host: this.options.host ?? {
|
|
448
469
|
name: "@memizy/host-sdk",
|
|
449
|
-
version: "1.0.0-rc.
|
|
470
|
+
version: "1.0.0-rc.7"
|
|
450
471
|
},
|
|
451
472
|
oqseVersion: "1.0",
|
|
452
473
|
features: [],
|
|
@@ -473,7 +494,8 @@ var g = class {
|
|
|
473
494
|
plugin: null,
|
|
474
495
|
set: null
|
|
475
496
|
},
|
|
476
|
-
snapshot: o
|
|
497
|
+
snapshot: o,
|
|
498
|
+
...e.address === this.authority && { seed: this.seed }
|
|
477
499
|
};
|
|
478
500
|
}
|
|
479
501
|
userKey(e) {
|
|
@@ -544,5 +566,9 @@ var g = class {
|
|
|
544
566
|
function _(e) {
|
|
545
567
|
return new Promise((t) => setTimeout(t, Math.max(0, e)));
|
|
546
568
|
}
|
|
569
|
+
function v() {
|
|
570
|
+
let e = /* @__PURE__ */ new Uint8Array(16);
|
|
571
|
+
return globalThis.crypto.getRandomValues(e), [...e].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
572
|
+
}
|
|
547
573
|
//#endregion
|
|
548
574
|
export { g as LocalSession };
|
package/dist/src/learning.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface LearningAlgorithm {
|
|
|
6
6
|
/**
|
|
7
7
|
* The new progress record of the answered item. `item` is the item of the set with its
|
|
8
8
|
* answers (type, difficulty, skills… for algorithms like FSRS); it may be ignored.
|
|
9
|
+
* Build it from `previous`: the learner's `starred` and fields of a later OQSEP version
|
|
10
|
+
* MUST stay (OQSEP "On unknown keys"); store `answer.answer` as `lastAnswer.answer`.
|
|
9
11
|
*/
|
|
10
12
|
apply(previous: ProgressRecord | undefined, answer: AnswerRecord, now: Date, item: OQSEAnyItem): ProgressRecord;
|
|
11
13
|
}
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface PreparedSet {
|
|
|
35
35
|
/**
|
|
36
36
|
* Keeps only the item types the plugin declares and reports the OQSEM
|
|
37
37
|
* handshake (`checkCompatibility`). The set should already be loaded with
|
|
38
|
-
* `loadOQSEFile`. With a `seed` (
|
|
38
|
+
* `loadOQSEFile`. With a secret `seed` (random per game, never the session id) the options, pairs
|
|
39
39
|
* and orders are shuffled once for the session (SPEC 4.4).
|
|
40
40
|
*/
|
|
41
41
|
export declare function prepareSetForPlugin(file: OQSEFile, manifest: PluginManifest, options?: {
|
package/dist/src/session.d.ts
CHANGED
|
@@ -31,10 +31,17 @@ export interface SessionConfig {
|
|
|
31
31
|
theme?: HostConfig['theme'];
|
|
32
32
|
sessionId?: string;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Secret seed of the display order of options, pairs… (SPEC 4.4; default: random). Pass the
|
|
35
|
+
* seed used for the relay bundle so all devices agree, and the same seed again to continue a
|
|
36
|
+
* saved game; `null` keeps the set's order. Never derive it from the session id or the PIN:
|
|
37
|
+
* a player could undo the shuffle.
|
|
36
38
|
*/
|
|
37
39
|
shuffleSeed?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Secret seed of the game's random numbers, given only to the authority (SPEC 4.2;
|
|
42
|
+
* default: 128 random bits). Pass one only to replay a game or in tests.
|
|
43
|
+
*/
|
|
44
|
+
seed?: string;
|
|
38
45
|
host?: {
|
|
39
46
|
name: string;
|
|
40
47
|
version: string;
|
|
@@ -84,14 +91,19 @@ export type SessionEvent = {
|
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
86
93
|
* A recorded answer. `record` is the updated learning progress (null for a generated item,
|
|
87
|
-
* which is not repeated); `
|
|
94
|
+
* which is not repeated, and for an ungraded answer); `ungraded`: an answer that is not
|
|
95
|
+
* checked automatically (`open-ended`, SPEC 5.1) – kept for a teacher, repetition unchanged.
|
|
96
|
+
* `setId` + `answer.itemId` identify the item (OQSE: an item ID alone is not unique across
|
|
97
|
+
* sets); `skills` / `tags` come from the item (for statistics).
|
|
88
98
|
*/
|
|
89
99
|
| {
|
|
90
100
|
type: 'answer';
|
|
91
101
|
playerId: string;
|
|
102
|
+
setId: string;
|
|
92
103
|
record: ProgressRecord | null;
|
|
93
104
|
answer: AnswerRecord;
|
|
94
105
|
generated: boolean;
|
|
106
|
+
ungraded: boolean;
|
|
95
107
|
skills: string[];
|
|
96
108
|
tags: string[];
|
|
97
109
|
}
|
|
@@ -142,6 +154,8 @@ export interface SessionInstance {
|
|
|
142
154
|
}
|
|
143
155
|
export declare class LocalSession {
|
|
144
156
|
readonly id: string;
|
|
157
|
+
/** Secret seed of the game's random numbers (only the authority gets it). */
|
|
158
|
+
private readonly seed;
|
|
145
159
|
readonly plugin: LoadedPlugin;
|
|
146
160
|
readonly runtime: PluginRuntime;
|
|
147
161
|
readonly mode: 'solo' | 'multiplayer';
|
package/dist/version.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=`1.0.0-rc.
|
|
1
|
+
var e=`1.0.0-rc.7`;exports.HOST_SDK_VERSION=e;
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memizy/host-sdk",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.7",
|
|
4
4
|
"description": "Run Memizy plugins: sandboxed iframes, protocol validation, sessions, storage hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@memizy/oqse": "^1.0.0-rc.
|
|
32
|
-
"@memizy/protocol": "^1.0.0-rc.
|
|
31
|
+
"@memizy/oqse": "^1.0.0-rc.7",
|
|
32
|
+
"@memizy/protocol": "^1.0.0-rc.7",
|
|
33
33
|
"penpal": "^8.0.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@memizy/plugin-sdk": "^1.0.0-rc.
|
|
36
|
+
"@memizy/plugin-sdk": "^1.0.0-rc.7",
|
|
37
37
|
"@types/node": "^25.6.0",
|
|
38
38
|
"@types/ws": "^8",
|
|
39
39
|
"jsdom": "^30.1.2",
|