@tomorrowos/sdk 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { TomorrowOS } from "./tomorrowos.js";
2
2
  export type { DeviceListItem, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
3
- export type { DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./store/types.js";
3
+ export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSStore } from "./store/types.js";
4
+ export { PlaylistCatalog } from "./playlist-catalog.js";
5
+ export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
4
6
  export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
5
7
  export { MemoryStore } from "./store/memory-store.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { TomorrowOS } from "./tomorrowos.js";
2
+ export { PlaylistCatalog } from "./playlist-catalog.js";
2
3
  export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
3
4
  export { MemoryStore } from "./store/memory-store.js";
@@ -0,0 +1,35 @@
1
+ import type { DevicePlaylistAssignment, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSStore } from "./store/types.js";
2
+ export interface SavePlaylistInput {
3
+ id?: string;
4
+ name: string;
5
+ schedule?: PlaylistSchedule;
6
+ items: PlaylistItemRecord[];
7
+ }
8
+ export interface BuiltDevicePolicy {
9
+ policy: {
10
+ playlists: PublishedPlaylistSnapshot[];
11
+ fallback: {
12
+ type: "brand";
13
+ };
14
+ revision?: number;
15
+ syncMode?: "latest" | "snapshot";
16
+ };
17
+ }
18
+ export declare class PlaylistCatalog {
19
+ private readonly store;
20
+ constructor(store: TomorrowOSStore);
21
+ listPlaylists(): Promise<StoredPlaylist[]>;
22
+ listPlaylistsIncludingRetired(): Promise<StoredPlaylist[]>;
23
+ getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
24
+ savePlaylist(input: SavePlaylistInput): Promise<StoredPlaylist>;
25
+ retirePlaylist(id: string): Promise<StoredPlaylist>;
26
+ getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
27
+ publishPlaylistsToDevice(deviceId: string, playlistIds: string[]): Promise<BuiltDevicePolicy>;
28
+ removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
29
+ buildPolicyForDevice(deviceId: string, options?: {
30
+ useLatest?: boolean;
31
+ }): Promise<BuiltDevicePolicy>;
32
+ private buildPolicyFromAssignments;
33
+ canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
34
+ }
35
+ //# sourceMappingURL=playlist-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAYD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,iBAAiB,CAAC;IA8BvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACpC,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IA+BxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
@@ -0,0 +1,133 @@
1
+ import { randomUUID } from "crypto";
2
+ function cloneSnapshot(playlist) {
3
+ return {
4
+ id: playlist.id,
5
+ name: playlist.name,
6
+ version: playlist.version,
7
+ schedule: playlist.schedule ? { ...playlist.schedule } : undefined,
8
+ items: playlist.items.map((item) => ({ ...item }))
9
+ };
10
+ }
11
+ export class PlaylistCatalog {
12
+ store;
13
+ constructor(store) {
14
+ this.store = store;
15
+ }
16
+ async listPlaylists() {
17
+ const all = await this.store.listPlaylists();
18
+ return all.filter((p) => !p.retired);
19
+ }
20
+ async listPlaylistsIncludingRetired() {
21
+ return this.store.listPlaylists();
22
+ }
23
+ async getPlaylist(id) {
24
+ return this.store.getPlaylist(id);
25
+ }
26
+ async savePlaylist(input) {
27
+ const name = String(input.name || "").trim();
28
+ if (!name) {
29
+ throw Object.assign(new Error("Playlist name is required"), {
30
+ code: "PLAYLIST_INVALID"
31
+ });
32
+ }
33
+ const id = input.id?.trim() || randomUUID();
34
+ const taken = await this.store.isPlaylistNameTaken(name, id);
35
+ if (taken) {
36
+ throw Object.assign(new Error("Playlist name already exists"), {
37
+ code: "PLAYLIST_NAME_CONFLICT"
38
+ });
39
+ }
40
+ const existing = await this.store.getPlaylist(id);
41
+ const now = new Date().toISOString();
42
+ const record = {
43
+ id,
44
+ name,
45
+ schedule: input.schedule,
46
+ items: Array.isArray(input.items) ? input.items : [],
47
+ version: (existing?.version ?? 0) + 1,
48
+ updatedAt: now,
49
+ retired: false
50
+ };
51
+ await this.store.setPlaylist(record);
52
+ return record;
53
+ }
54
+ async retirePlaylist(id) {
55
+ const existing = await this.store.getPlaylist(id);
56
+ if (!existing) {
57
+ throw Object.assign(new Error("Playlist not found"), { code: "PLAYLIST_NOT_FOUND" });
58
+ }
59
+ const record = {
60
+ ...existing,
61
+ retired: true,
62
+ retiredAt: new Date().toISOString()
63
+ };
64
+ await this.store.setPlaylist(record);
65
+ return record;
66
+ }
67
+ async getDeviceAssignments(deviceId) {
68
+ return this.store.getDeviceAssignments(deviceId);
69
+ }
70
+ async publishPlaylistsToDevice(deviceId, playlistIds) {
71
+ const ids = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
72
+ if (ids.length === 0) {
73
+ throw Object.assign(new Error("Select at least one playlist"), {
74
+ code: "PLAYLIST_INVALID"
75
+ });
76
+ }
77
+ const assignments = [];
78
+ for (const playlistId of ids) {
79
+ const playlist = await this.store.getPlaylist(playlistId);
80
+ if (!playlist || playlist.retired) {
81
+ throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
82
+ code: "PLAYLIST_NOT_FOUND"
83
+ });
84
+ }
85
+ assignments.push({
86
+ playlistId,
87
+ publishedVersion: playlist.version,
88
+ publishedAt: new Date().toISOString(),
89
+ snapshot: cloneSnapshot(playlist)
90
+ });
91
+ }
92
+ await this.store.setDeviceAssignments(deviceId, assignments);
93
+ return this.buildPolicyFromAssignments(assignments, { useLatest: false });
94
+ }
95
+ async removePlaylistFromDevice(deviceId, playlistId) {
96
+ const assignments = await this.store.getDeviceAssignments(deviceId);
97
+ const next = assignments.filter((a) => a.playlistId !== playlistId);
98
+ await this.store.setDeviceAssignments(deviceId, next);
99
+ return this.buildPolicyFromAssignments(next, { useLatest: false });
100
+ }
101
+ async buildPolicyForDevice(deviceId, options = {}) {
102
+ const assignments = await this.store.getDeviceAssignments(deviceId);
103
+ return this.buildPolicyFromAssignments(assignments, options);
104
+ }
105
+ async buildPolicyFromAssignments(assignments, options) {
106
+ const useLatest = options.useLatest === true;
107
+ const playlists = [];
108
+ for (const assignment of assignments) {
109
+ if (useLatest) {
110
+ const current = await this.store.getPlaylist(assignment.playlistId);
111
+ if (current && !current.retired) {
112
+ playlists.push(cloneSnapshot(current));
113
+ continue;
114
+ }
115
+ }
116
+ playlists.push({
117
+ ...assignment.snapshot,
118
+ items: assignment.snapshot.items.map((item) => ({ ...item }))
119
+ });
120
+ }
121
+ return {
122
+ policy: {
123
+ playlists,
124
+ fallback: { type: "brand" },
125
+ revision: Date.now(),
126
+ syncMode: useLatest ? "latest" : "snapshot"
127
+ }
128
+ };
129
+ }
130
+ canPublishPlaylistToNewDevice(playlistId) {
131
+ return this.store.getPlaylist(playlistId).then((p) => !!p && !p?.retired);
132
+ }
133
+ }
@@ -1,4 +1,4 @@
1
- import type { DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./types.js";
1
+ import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist, TomorrowOSStore } from "./types.js";
2
2
  /**
3
3
  * Default store for development / single-node demos.
4
4
  * Data is lost on process restart — not for multi-instance production.
@@ -8,6 +8,8 @@ export declare class MemoryStore implements TomorrowOSStore {
8
8
  private readonly deviceRegistry;
9
9
  private readonly codeToDeviceId;
10
10
  private readonly pairedDevices;
11
+ private readonly playlists;
12
+ private readonly deviceAssignments;
11
13
  setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
12
14
  getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
13
15
  deletePendingCode(code: string): Promise<void>;
@@ -21,5 +23,11 @@ export declare class MemoryStore implements TomorrowOSStore {
21
23
  getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
22
24
  deletePairedDevice(deviceId: string): Promise<void>;
23
25
  listPairedDevices(): Promise<PairedDeviceEntry[]>;
26
+ listPlaylists(): Promise<StoredPlaylist[]>;
27
+ getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
28
+ setPlaylist(record: StoredPlaylist): Promise<void>;
29
+ isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
30
+ getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
31
+ setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
24
32
  }
25
33
  //# sourceMappingURL=memory-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IAEjE,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAMxD"}
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;CASjB"}
@@ -7,6 +7,8 @@ export class MemoryStore {
7
7
  deviceRegistry = new Map();
8
8
  codeToDeviceId = new Map();
9
9
  pairedDevices = new Map();
10
+ playlists = new Map();
11
+ deviceAssignments = new Map();
10
12
  async setPendingCode(code, record) {
11
13
  this.pendingCodes.set(code, record);
12
14
  }
@@ -51,4 +53,37 @@ export class MemoryStore {
51
53
  record
52
54
  }));
53
55
  }
56
+ async listPlaylists() {
57
+ return [...this.playlists.values()];
58
+ }
59
+ async getPlaylist(id) {
60
+ return this.playlists.get(id);
61
+ }
62
+ async setPlaylist(record) {
63
+ this.playlists.set(record.id, record);
64
+ }
65
+ async isPlaylistNameTaken(name, excludeId) {
66
+ const target = name.trim().toLowerCase();
67
+ for (const playlist of this.playlists.values()) {
68
+ if (playlist.retired)
69
+ continue;
70
+ if (excludeId && playlist.id === excludeId)
71
+ continue;
72
+ if (playlist.name.trim().toLowerCase() === target)
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+ async getDeviceAssignments(deviceId) {
78
+ return [...(this.deviceAssignments.get(deviceId) ?? [])];
79
+ }
80
+ async setDeviceAssignments(deviceId, assignments) {
81
+ this.deviceAssignments.set(deviceId, assignments.map((a) => ({
82
+ ...a,
83
+ snapshot: {
84
+ ...a.snapshot,
85
+ items: a.snapshot.items.map((item) => ({ ...item }))
86
+ }
87
+ })));
88
+ }
54
89
  }
@@ -29,6 +29,41 @@ export interface PairedDeviceEntry {
29
29
  deviceId: string;
30
30
  record: PairedDeviceRecord;
31
31
  }
32
+ export interface PlaylistItemRecord {
33
+ url: string;
34
+ type?: string;
35
+ durationMs?: number;
36
+ }
37
+ export interface PlaylistSchedule {
38
+ startDate?: string;
39
+ endDate?: string;
40
+ daysOfWeek?: number[];
41
+ start?: string;
42
+ end?: string;
43
+ }
44
+ export interface StoredPlaylist {
45
+ id: string;
46
+ name: string;
47
+ schedule?: PlaylistSchedule;
48
+ items: PlaylistItemRecord[];
49
+ version: number;
50
+ updatedAt: string;
51
+ retired?: boolean;
52
+ retiredAt?: string;
53
+ }
54
+ export interface PublishedPlaylistSnapshot {
55
+ id: string;
56
+ name: string;
57
+ version: number;
58
+ schedule?: PlaylistSchedule;
59
+ items: PlaylistItemRecord[];
60
+ }
61
+ export interface DevicePlaylistAssignment {
62
+ playlistId: string;
63
+ publishedVersion: number;
64
+ publishedAt: string;
65
+ snapshot: PublishedPlaylistSnapshot;
66
+ }
32
67
  /**
33
68
  * Implement with Postgres, Redis, etc. Defaults to MemoryStore (Map).
34
69
  * All methods async so DB backends do not need sync adapters.
@@ -47,5 +82,11 @@ export interface TomorrowOSStore {
47
82
  getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
48
83
  deletePairedDevice(deviceId: string): Promise<void>;
49
84
  listPairedDevices(): Promise<PairedDeviceEntry[]>;
85
+ listPlaylists(): Promise<StoredPlaylist[]>;
86
+ getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
87
+ setPlaylist(record: StoredPlaylist): Promise<void>;
88
+ isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
89
+ getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
90
+ setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
50
91
  }
51
92
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAC;IAC/E,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uBAAuB,CACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAC3E,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC3E,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CACnD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
2
  import http from "http";
3
+ import { PlaylistCatalog, type BuiltDevicePolicy } from "./playlist-catalog.js";
3
4
  import type { TomorrowOSStore } from "./store/types.js";
4
5
  export interface TomorrowOSBrand {
5
6
  name?: string;
@@ -45,10 +46,17 @@ export interface DeviceListItem {
45
46
  screenOnlineLabel: string;
46
47
  /** Same as lastBootAt while connected; used for uptime display. */
47
48
  screenOnlineSince: string | null;
49
+ publishedPlaylists: Array<{
50
+ playlistId: string;
51
+ name: string;
52
+ version: number;
53
+ publishedAt: string;
54
+ }>;
48
55
  }
49
56
  export declare class TomorrowOS extends EventEmitter {
50
57
  readonly brand: TomorrowOSBrand;
51
58
  private readonly store;
59
+ readonly playlists: PlaylistCatalog;
52
60
  private readonly devices;
53
61
  private readonly pendingDeviceMeta;
54
62
  private httpServer;
@@ -56,6 +64,12 @@ export declare class TomorrowOS extends EventEmitter {
56
64
  private staticRoot;
57
65
  private staticIndexFile;
58
66
  constructor(options: TomorrowOSOptions);
67
+ /** Push all device assignments using latest playlist definitions (e.g. after reboot). */
68
+ pushLatestPolicyToDevice(deviceId: string): Promise<{
69
+ pushed: boolean;
70
+ policy?: BuiltDevicePolicy["policy"];
71
+ }>;
72
+ private sendDeviceCommand;
59
73
  /** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
60
74
  pairingVerify(code: string): Promise<{
61
75
  deviceId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@ import http from "http";
5
5
  import path from "path";
6
6
  import { WebSocket, WebSocketServer } from "ws";
7
7
  import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode } from "./pairing-code.js";
8
+ import { PlaylistCatalog } from "./playlist-catalog.js";
8
9
  import { MemoryStore } from "./store/memory-store.js";
9
10
  function formatDurationMs(ms) {
10
11
  if (!Number.isFinite(ms) || ms < 0)
@@ -80,6 +81,7 @@ function parseDevicePath(pathname) {
80
81
  export class TomorrowOS extends EventEmitter {
81
82
  brand;
82
83
  store;
84
+ playlists;
83
85
  devices = new Map();
84
86
  pendingDeviceMeta = new Map();
85
87
  httpServer = null;
@@ -90,6 +92,31 @@ export class TomorrowOS extends EventEmitter {
90
92
  super();
91
93
  this.brand = options.brand;
92
94
  this.store = options.store ?? new MemoryStore();
95
+ this.playlists = new PlaylistCatalog(this.store);
96
+ }
97
+ /** Push all device assignments using latest playlist definitions (e.g. after reboot). */
98
+ async pushLatestPolicyToDevice(deviceId) {
99
+ const id = String(deviceId || "").trim();
100
+ const assignments = await this.store.getDeviceAssignments(id);
101
+ if (assignments.length === 0)
102
+ return { pushed: false };
103
+ const built = await this.playlists.buildPolicyForDevice(id, { useLatest: true });
104
+ const ws = this.devices.get(id);
105
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
106
+ return { pushed: false, policy: built.policy };
107
+ }
108
+ await this.sendDeviceCommand(id, "device.content.setPolicy", {
109
+ policy: built.policy
110
+ });
111
+ await this.recordPolicyPush(id);
112
+ return { pushed: true, policy: built.policy };
113
+ }
114
+ async sendDeviceCommand(deviceId, method, params) {
115
+ const ws = this.devices.get(deviceId);
116
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
117
+ throw Object.assign(new Error("Device not connected"), { code: "DEVICE_OFFLINE" });
118
+ }
119
+ return this.sendCommandToSocket(ws, deviceId, method, params);
93
120
  }
94
121
  /** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
95
122
  async pairingVerify(code) {
@@ -175,6 +202,7 @@ export class TomorrowOS extends EventEmitter {
175
202
  return Promise.all(entries.map(async ({ deviceId, record }) => {
176
203
  const connected = this.isDeviceConnected(deviceId);
177
204
  const reg = await this.store.getDeviceRegistry(deviceId);
205
+ const assignments = await this.store.getDeviceAssignments(deviceId);
178
206
  let screenOnlineActive = false;
179
207
  let screenOnlineLabel = "Not active";
180
208
  if (connected && record.lastBootAt) {
@@ -188,6 +216,12 @@ export class TomorrowOS extends EventEmitter {
188
216
  return {
189
217
  deviceId,
190
218
  pairingCode: reg?.permanentPairingCode ?? null,
219
+ publishedPlaylists: assignments.map((a) => ({
220
+ playlistId: a.playlistId,
221
+ name: a.snapshot.name,
222
+ version: a.publishedVersion,
223
+ publishedAt: a.publishedAt
224
+ })),
191
225
  connected,
192
226
  deviceName: record.deviceName ?? null,
193
227
  platform: record.platform ?? null,
@@ -550,6 +584,105 @@ export class TomorrowOS extends EventEmitter {
550
584
  sendJson(res, 200, { status: "success", devices });
551
585
  return;
552
586
  }
587
+ if (req.method === "GET" && pathname === "/playlists") {
588
+ const playlists = await this.playlists.listPlaylists();
589
+ sendJson(res, 200, { status: "success", playlists });
590
+ return;
591
+ }
592
+ if (req.method === "POST" && pathname === "/playlists") {
593
+ const body = (await readJsonBody(req));
594
+ try {
595
+ const saved = await this.playlists.savePlaylist({
596
+ id: typeof body.id === "string" ? body.id : undefined,
597
+ name: String(body.name ?? ""),
598
+ schedule: body.schedule && typeof body.schedule === "object"
599
+ ? body.schedule
600
+ : undefined,
601
+ items: Array.isArray(body.items) ? body.items : []
602
+ });
603
+ sendJson(res, 200, { status: "success", playlist: saved });
604
+ }
605
+ catch (e) {
606
+ const msg = e instanceof Error ? e.message : "Save failed";
607
+ sendJson(res, 400, { status: "failed", error: msg });
608
+ }
609
+ return;
610
+ }
611
+ const playlistDelete = /^\/playlists\/([^/]+)$/.exec(pathname);
612
+ if (req.method === "DELETE" && playlistDelete) {
613
+ const playlistId = decodeURIComponent(playlistDelete[1]);
614
+ try {
615
+ const retired = await this.playlists.retirePlaylist(playlistId);
616
+ sendJson(res, 200, { status: "success", playlist: retired });
617
+ }
618
+ catch (e) {
619
+ const msg = e instanceof Error ? e.message : "Delete failed";
620
+ sendJson(res, 400, { status: "failed", error: msg });
621
+ }
622
+ return;
623
+ }
624
+ const deviceAssignmentsGet = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
625
+ if (req.method === "GET" && deviceAssignmentsGet) {
626
+ const deviceId = decodeURIComponent(deviceAssignmentsGet[1]);
627
+ const assignments = await this.playlists.getDeviceAssignments(deviceId);
628
+ sendJson(res, 200, { status: "success", assignments });
629
+ return;
630
+ }
631
+ const deviceAssignmentsPost = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
632
+ if (req.method === "POST" && deviceAssignmentsPost) {
633
+ const deviceId = decodeURIComponent(deviceAssignmentsPost[1]);
634
+ const body = (await readJsonBody(req));
635
+ const ws = this.devices.get(deviceId);
636
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
637
+ sendJson(res, 404, { status: "failed", error: "Device not connected" });
638
+ return;
639
+ }
640
+ try {
641
+ let built;
642
+ if (body.useLatest === true) {
643
+ built = await this.playlists.buildPolicyForDevice(deviceId, {
644
+ useLatest: true
645
+ });
646
+ }
647
+ else {
648
+ const ids = Array.isArray(body.playlistIds) ? body.playlistIds : [];
649
+ built = await this.playlists.publishPlaylistsToDevice(deviceId, ids);
650
+ }
651
+ const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
652
+ policy: built.policy
653
+ });
654
+ await this.recordPolicyPush(deviceId);
655
+ sendJson(res, 200, { status: "success", policy: built.policy, result });
656
+ }
657
+ catch (e) {
658
+ const msg = e instanceof Error ? e.message : "Publish failed";
659
+ sendJson(res, 400, { status: "failed", error: msg });
660
+ }
661
+ return;
662
+ }
663
+ const deviceAssignmentDelete = /^\/device\/([^/]+)\/assignments\/([^/]+)$/.exec(pathname);
664
+ if (req.method === "DELETE" && deviceAssignmentDelete) {
665
+ const deviceId = decodeURIComponent(deviceAssignmentDelete[1]);
666
+ const playlistId = decodeURIComponent(deviceAssignmentDelete[2]);
667
+ const ws = this.devices.get(deviceId);
668
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
669
+ sendJson(res, 404, { status: "failed", error: "Device not connected" });
670
+ return;
671
+ }
672
+ try {
673
+ const built = await this.playlists.removePlaylistFromDevice(deviceId, playlistId);
674
+ const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
675
+ policy: built.policy
676
+ });
677
+ await this.recordPolicyPush(deviceId);
678
+ sendJson(res, 200, { status: "success", policy: built.policy, result });
679
+ }
680
+ catch (e) {
681
+ const msg = e instanceof Error ? e.message : "Remove failed";
682
+ sendJson(res, 400, { status: "failed", error: msg });
683
+ }
684
+ return;
685
+ }
553
686
  if (req.method === "POST") {
554
687
  const parsed = parseDevicePath(pathname);
555
688
  if (parsed) {
@@ -701,6 +834,9 @@ export class TomorrowOS extends EventEmitter {
701
834
  this.sendBrandSnapshot(ws);
702
835
  void this.refreshPairedDeviceInfo(deviceId);
703
836
  this.emit("device.online", { deviceId });
837
+ void this.pushLatestPolicyToDevice(deviceId).catch((err) => {
838
+ console.error("[TomorrowOS] pushLatestPolicy on resume failed:", err);
839
+ });
704
840
  })();
705
841
  }
706
842
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "TomorrowOS CMS server SDK — WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",