Skip to content

Commit

Permalink
added AssignFrom, updates
Browse files Browse the repository at this point in the history
  • Loading branch information
patrick-rodgers committed Oct 18, 2021
1 parent 41eb647 commit fe58a93
Show file tree
Hide file tree
Showing 53 changed files with 756 additions and 160 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"runtimeArgs": [],
"sourceMaps": true,
"outFiles": [],
"args": ["--noretries", "--verbose", "--cleanup","--logging"],
"args": ["--noretries", "--verbose", "--cleanup","--logging", "--packages", "queryable", "--skip-web"],
"console": "internalConsole",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": [
Expand Down
5 changes: 4 additions & 1 deletion debug/launch/v3-patrick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/files";
import "@pnp/sp/folders";
import "@pnp/sp/appcatalog";

declare var process: { exit(code?: number): void };

Expand Down Expand Up @@ -49,7 +50,9 @@ export async function Example(settings: ITestingSettings) {
},
})).using(PnPLogging(LogLevel.Verbose));

const y = await sp2.web();
const web = await sp2.getTenantAppCatalogWeb();

const y = await web();

console.log(JSON.stringify(y));

Expand Down
129 changes: 119 additions & 10 deletions docs/core/timeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ The `first` moment uses a pre-defined moment implementation `asyncReduce`. This
import { asyncReduce, ObserverAction, Timeline } from "@pnp/core";

// the first observer is a function taking a number and async returning a number in an array
// all asyncReduce observers must follow this patter of returning async a tuple matching the args
export type FirstObserver = (this: any, counter: number) => Promise<[number]>;

// the second observer is a function taking a number and returning void
export type SecondObserver = (this: any, result: number) => void;

// this is a custom moment definition as an example. Please read more about moments on the moments page
export function report<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {

return function (observers: T[], ...args: any[]): void {
Expand All @@ -37,39 +39,146 @@ export function report<T extends ObserverAction>(): (observers: T[], ...args: an
};
}

const MyMoments = {
// this plain object defines the moments which will be available in our timeline
// the property name "first" and "second" will be the moment names, used when we make calls such as instance.on.first and instance.on.second
const TestingMoments = {
first: asyncReduce<FirstObserver>(),
second: report<SecondObserver>(),
} as const;
// note as well the use of as const, this allows TypeScript to properly resolve all the complex typings and not treat the plain object as "any"
```

### Subclass Timeline

After defining our moments we need to subclass Timeline to define how those moments emit through the lifecycle of the Timeline. Timeline has a single abstract method "execute" you must implement. You will also need to provide a way for callers to trigger the protected "start" method.

```TypeScript
// our implementation of timeline, note we use `typeof TestingMoments` and ALSO pass the testing moments object to super() in the constructor
class TestTimeline extends Timeline<typeof TestingMoments> {

// we create two unique refs for our implementation we will use
// to resolve the execute promise
private InternalResolveEvent = Symbol.for("Resolve");
private InternalRejectEvent = Symbol.for("Reject");

constructor() {
// we need to pass the moments to the base Timeline
super(TestingMoments);
}

// we implement the execute the method to define when, in what order, and how our moments are called. This give you full control within the Timeline framework
// to determine your implementation's behavior
protected async execute(init?: any): Promise<any> {

// we can always emit log to any subscribers
this.log("Starting", 0);

// set our timeline to start in the next tick
setTimeout(async () => {

try {

// we emit our "first" event
let [value] = await this.emit.first(init);

// we emit our "second" event
[value] = await this.emit.second(value);

// we reolve the execute promise with the final value
this.emit[this.InternalResolveEvent](value);

} catch (e) {

// we emit our reject event
this.emit[this.InternalRejectEvent](e);
// we emit error to any subscribed observers
this.error(e);
}
}, 0);

// return a promise which we will resolve/reject during the timeline lifecycle
return new Promise((resolve, reject) => {
this.on[this.InternalResolveEvent].replace(resolve);
this.on[this.InternalRejectEvent].replace(reject);
});
}

// provide a method to trigger our timeline, this could be protected or called directly by the user, your choice
public go(startValue = 0): Promise<number> {

// here we take a starting number
return this.start(startValue);
}
}
```

### Using your Timeline

```TypeScript
import { TestTimeline } from "./file.js";

const tl = new TestTimeline();

// register observer
tl.on.first(async (n) => [++n]);

// register observer
tl.on.second(async (n) => [++n]);

// h === 2
const h = await tl.go(0);

// h === 7
const h2 = await tl.go(5);
```

## Understanding the Timline Lifecycle

Now that you implemented a simple timeline let's take a minute to understand the lifecycle of a timeline execution. There are four moments always defined for every timeline: init, dispose, log, and error. Of these init and dispose are used within the lifecycle, while log and error are used as you need.

### Timeline Lifecycle

- init
- your moments as defined in execute, in our example
- first
- second
- dispose

Timeline - intro, what is it conceptually
## Observer Inheritance

describe the parts -> timeline, moments definition, observers
Let's say that you want to contruct a system whereby you can create Timeline based instances from other Timeline based instances - which is what [Queryable](../queryable/queryable.md) does. Imagine we have a class with a pseudo-signature like:

show custom timeline
- moments def
- execute impl
```TypeScript
class ATimeline extends Timeline<typeof SomeMoments> {

- show observer registration
// we create two unique refs for our implementation we will use
// to resolve the execute promise
private InternalResolveEvent = Symbol.for("Resolve");
private InternalRejectEvent = Symbol.for("Reject");

- show behavior registering multiple observers
constructor(base: ATimeline) {

- link to batching docs and code as an example of a complex behavior
- link to other behaviors to show what's what
// we need to pass the moments to the base Timeline
super(TestingMoments, base.observers);
}

//...
}
```

We can then use it like:

```TypeScript
const tl1 = new ATimeline();
tl1.on.first(async (n) => [++n]);
tl1.on.second(async (n) => [++n]);

// at this point tl2's observer collection is a pointer/reference to the same collection as tl1
const tl2 = new ATimeline(tl1);

// we add a second observer to first, it is applied to BOTH tl1 and tl2
tl1.on.first(async (n) => [++n]);

// BUT when we modify tl2's observers, either by adding or clearing a moment it begins to track its own collection
tl2.on.first(async (n) => [++n]);
```
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"lodash.clonedeep": "^4.5.0",
"mocha": "^9.1.2",
"msal": "^1.4.6",
"node-abort-controller": "^3.0.1",
"node-fetch": "^2.6.1",
"prettyjson": "^1.2.1",
"string-replace-loader": "^3.0.1",
Expand Down
17 changes: 17 additions & 0 deletions packages/core/behaviors/assign-from.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Timeline, TimelinePipe } from "@pnp/core";

/**
* Behavior that will assign a ref to the source's observers and reset the instance's inheriting flag
*
* @param source The source instance from which we will assign the observers
*/
export function AssignFrom(source: Timeline<any>): TimelinePipe {

return (instance: Timeline<any>) => {

(<any>instance).observers = (<any>source).observers;
(<any>instance)._inheritingObservers = true;

return instance;
};
}
File renamed without changes.
3 changes: 2 additions & 1 deletion packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export * from "./extendable.js";
/**
* Behavior exports
*/
export * from "./behaviors/copyfrom.js";
export * from "./behaviors/assign-from.js";
export * from "./behaviors/copy-from.js";
8 changes: 4 additions & 4 deletions packages/core/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type DistributeEmit<T extends Moments> =
/**
* Virtual events that are present on all Timelines
*/
type DefaultTimelineEvents<T extends Moments> = {
type DefaultTimelineMoments<T extends Moments> = {
init: (observers: ((this: Timeline<T>) => void)[], ...args: any[]) => void;
dispose: (observers: ((this: Timeline<T>) => void)[], ...args: any[]) => void;
log: (observers: ((this: Timeline<T>, message: string, level: number) => void)[], ...args: any[]) => void;
Expand All @@ -60,12 +60,12 @@ type DefaultTimelineEvents<T extends Moments> = {
/**
* The type combining the defined moments and DefaultTimelineEvents
*/
type OnProxyType<T extends Moments> = DistributeOn<T> & DistributeOn<DefaultTimelineEvents<T>, T>;
type OnProxyType<T extends Moments> = DistributeOn<T> & DistributeOn<DefaultTimelineMoments<T>, T>;

/**
* The type combining the defined moments and DefaultTimelineEvents
*/
type EmitProxyType<T extends Moments> = DistributeEmit<T> & DistributeEmit<DefaultTimelineEvents<T>>;
type EmitProxyType<T extends Moments> = DistributeEmit<T> & DistributeEmit<DefaultTimelineMoments<T>>;

/**
* Represents a function accepting and returning a timeline, possibly manipulating the observers present
Expand Down Expand Up @@ -129,7 +129,7 @@ export abstract class Timeline<T extends Moments> {

if (Reflect.has(target.observers, p)) {
target.cloneObserversOnChange();
// we trust outselves that this will be an array
// we trust ourselves that this will be an array
(<ObserverCollection>target.observers)[p].length = 0;
return true;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/graph/onedrive/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
GraphQueryable,
} from "../graphqueryable.js";
import { Drive as IDriveType } from "@microsoft/microsoft-graph-types";
import { combine } from "@pnp/core";
import { combine, AssignFrom } from "@pnp/core";
import { defaultPath, getById, IGetById, deleteable, IDeleteable, updateable, IUpdateable } from "../decorators.js";
import { body, CopyFromQueryable, BlobParse } from "@pnp/queryable";
import { body, BlobParse } from "@pnp/queryable";
import { graphPatch, graphPut } from "../operations.js";

/**
Expand Down Expand Up @@ -108,7 +108,7 @@ export class _DriveItem extends _GraphQueryableInstance<any> {
// TODO:: make sure this works
public async getContent(): Promise<Blob> {
const info = await this();
const query = GraphQueryable(info["@microsoft.graph.downloadUrl"], null).using(BlobParse).using(CopyFromQueryable(this));
const query = GraphQueryable(info["@microsoft.graph.downloadUrl"], null).using(BlobParse).using(AssignFrom(this));

query.on.pre(async (url, init, result) => {

Expand Down
9 changes: 4 additions & 5 deletions packages/nodejs/sp-extensions/stream.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { getGUID, isFunc } from "@pnp/core/util";
import { CopyFromQueryable, headers } from "@pnp/queryable";
import { headers } from "@pnp/queryable";
import { File, Files, IFile, IFileAddResult, IFileInfo, IFiles, IFileUploadProgressData } from "@pnp/sp/files";
import { spPost } from "@pnp/sp/operations";
import { escapeQueryStrValue } from "@pnp/sp/utils/escapeQueryStrValue";
import { ReadStream } from "fs";
import { PassThrough } from "stream";
import { extendFactory } from "@pnp/core";
import { odataUrlFrom } from "@pnp/sp";
import { AssignFrom, extendFactory } from "@pnp/core";
import { odataUrlFrom, escapeQueryStrValue } from "@pnp/sp";
import { StreamParse } from "../behaviors/stream-parse.js";

export interface IResponseBodyStream {
Expand Down Expand Up @@ -87,7 +86,7 @@ extendFactory(Files, {
): Promise<IFileAddResult> {

const response: IFileInfo = await spPost(Files(this, `add(overwrite=${shouldOverWrite},url='${escapeQueryStrValue(url)}')`));
const file = File(odataUrlFrom(response)).using(CopyFromQueryable(this));
const file = File(odataUrlFrom(response)).using(AssignFrom(this));

if ("function" === typeof (content as ReadStream).read) {
return file.setStreamContentChunked(content as ReadStream, progress);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function CachingPessimisticRefresh(

const emitError = (e) => {
this.emit.log(`[id:${requestId}] Emitting error: "${e.message || e}"`, 3);
this.emit[this.InternalRejectEvent](e);
this.emit.error(e);
this.emit.log(`[id:${requestId}] Emitted error: "${e.message || e}"`, 3);
};
Expand Down Expand Up @@ -108,6 +109,7 @@ export function CachingPessimisticRefresh(

const emitData = () => {
this.emit.log(`[id:${requestId}] Emitting data`, 0);
this.emit[this.InternalResolveEvent](retVal);
this.emit.data(retVal);
this.emit.log(`[id:${requestId}] Emitted data`, 0);
};
Expand Down Expand Up @@ -160,8 +162,8 @@ export function CachingPessimisticRefresh(
}, 0);

return new Promise((resolve, reject) => {
this.on.data(resolve);
this.on.error(reject);
this.on[this.InternalResolveEvent].replace(resolve);
this.on[this.InternalRejectEvent].replace(reject);
});
},
});
Expand Down
25 changes: 0 additions & 25 deletions packages/queryable/behaviors/queryable-copyfrom.ts

This file was deleted.

19 changes: 19 additions & 0 deletions packages/queryable/behaviors/throw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TimelinePipe } from "../../core/timeline.js";
import { Queryable } from "../queryable.js";

export function ThrowErrors(): TimelinePipe {

return (instance: Queryable) => {

instance.on.pre(async function (this: Queryable, url, init, result) {

this.on.error((err) => {
throw err;
});

return [url, init, result];
});

return instance;
};
}
Loading

0 comments on commit fe58a93

Please sign in to comment.